<!DOCTYPE html><html lang="zh-CN" data-theme="light"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"><title>JVM 番外-垃圾回收独立篇 | 无人深空</title><meta name="keywords" content="JVM"><meta name="author" content="zcm"><meta name="copyright" content="zcm"><meta name="format-detection" content="telephone=no"><meta name="theme-color" content="#ffffff"><meta name="description" content="JVM 上篇-垃圾回收 看 Java 不同版本新特性  语法层面：Lambda 表达式、switch、自动装箱、自动拆箱、enum、&lt;&gt;…… API 层面：Stream API、新的日期时间、Optional、String、集合框架…… 底层优化：JVM 的优化、GC 的变化、元空间、静态域、字符串常量池……    1.垃圾回收概述 垃圾收集，不是 Java 语言的伴生产物。早在 19">
<meta property="og:type" content="article">
<meta property="og:title" content="JVM 番外-垃圾回收独立篇">
<meta property="og:url" content="https://gitee.com/zcmmmm/zcmmmm/2022/10/07/JVM%20%E7%95%AA%E5%A4%96-%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E7%8B%AC%E7%AB%8B%E7%AF%87/index.html">
<meta property="og:site_name" content="无人深空">
<meta property="og:description" content="JVM 上篇-垃圾回收 看 Java 不同版本新特性  语法层面：Lambda 表达式、switch、自动装箱、自动拆箱、enum、&lt;&gt;…… API 层面：Stream API、新的日期时间、Optional、String、集合框架…… 底层优化：JVM 的优化、GC 的变化、元空间、静态域、字符串常量池……    1.垃圾回收概述 垃圾收集，不是 Java 语言的伴生产物。早在 19">
<meta property="og:locale" content="zh_CN">
<meta property="og:image" content="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/JVM.jpg">
<meta property="article:published_time" content="2022-10-07T00:29:46.000Z">
<meta property="article:modified_time" content="2022-10-08T06:28:49.712Z">
<meta property="article:author" content="zcm">
<meta property="article:tag" content="JVM">
<meta name="twitter:card" content="summary">
<meta name="twitter:image" content="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/JVM.jpg"><link rel="shortcut icon" href="/img/favicon.png"><link rel="canonical" href="https://gitee.com/zcmmmm/zcmmmm/2022/10/07/JVM%20%E7%95%AA%E5%A4%96-%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E7%8B%AC%E7%AB%8B%E7%AF%87/"><link rel="preconnect" href="//cdn.jsdelivr.net"/><link rel="preconnect" href="//busuanzi.ibruce.info"/><link rel="stylesheet" href="/css/index.css"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free/css/all.min.css" media="print" onload="this.media='all'"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/node-snackbar/dist/snackbar.min.css" media="print" onload="this.media='all'"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fancyapps/ui/dist/fancybox.min.css" media="print" onload="this.media='all'"><script>const GLOBAL_CONFIG = { 
  root: '/',
  algolia: undefined,
  localSearch: {"path":"/search.xml","preload":false,"languages":{"hits_empty":"找不到您查询的内容：${query}"}},
  translate: undefined,
  noticeOutdate: undefined,
  highlight: {"plugin":"highlighjs","highlightCopy":true,"highlightLang":true,"highlightHeightLimit":200},
  copy: {
    success: '复制成功',
    error: '复制错误',
    noSupport: '浏览器不支持'
  },
  relativeDate: {
    homepage: false,
    post: false
  },
  runtime: '天',
  date_suffix: {
    just: '刚刚',
    min: '分钟前',
    hour: '小时前',
    day: '天前',
    month: '个月前'
  },
  copyright: undefined,
  lightbox: 'fancybox',
  Snackbar: {"chs_to_cht":"你已切换为繁体","cht_to_chs":"你已切换为简体","day_to_night":"你已切换为深色模式","night_to_day":"你已切换为浅色模式","bgLight":"#49b1f5","bgDark":"#1f1f1f","position":"bottom-left"},
  source: {
    justifiedGallery: {
      js: 'https://cdn.jsdelivr.net/npm/flickr-justified-gallery/dist/fjGallery.min.js',
      css: 'https://cdn.jsdelivr.net/npm/flickr-justified-gallery/dist/fjGallery.min.css'
    }
  },
  isPhotoFigcaption: false,
  islazyload: false,
  isAnchor: false
}</script><script id="config-diff">var GLOBAL_CONFIG_SITE = {
  title: 'JVM 番外-垃圾回收独立篇',
  isPost: true,
  isHome: false,
  isHighlightShrink: false,
  isToc: true,
  postUpdate: '2022-10-08 14:28:49'
}</script><noscript><style type="text/css">
  #nav {
    opacity: 1
  }
  .justified-gallery img {
    opacity: 1
  }

  #recent-posts time,
  #post-meta time {
    display: inline !important
  }
</style></noscript><script>(win=>{
    win.saveToLocal = {
      set: function setWithExpiry(key, value, ttl) {
        if (ttl === 0) return
        const now = new Date()
        const expiryDay = ttl * 86400000
        const item = {
          value: value,
          expiry: now.getTime() + expiryDay,
        }
        localStorage.setItem(key, JSON.stringify(item))
      },

      get: function getWithExpiry(key) {
        const itemStr = localStorage.getItem(key)

        if (!itemStr) {
          return undefined
        }
        const item = JSON.parse(itemStr)
        const now = new Date()

        if (now.getTime() > item.expiry) {
          localStorage.removeItem(key)
          return undefined
        }
        return item.value
      }
    }
  
    win.getScript = url => new Promise((resolve, reject) => {
      const script = document.createElement('script')
      script.src = url
      script.async = true
      script.onerror = reject
      script.onload = script.onreadystatechange = function() {
        const loadState = this.readyState
        if (loadState && loadState !== 'loaded' && loadState !== 'complete') return
        script.onload = script.onreadystatechange = null
        resolve()
      }
      document.head.appendChild(script)
    })
  
      win.activateDarkMode = function () {
        document.documentElement.setAttribute('data-theme', 'dark')
        if (document.querySelector('meta[name="theme-color"]') !== null) {
          document.querySelector('meta[name="theme-color"]').setAttribute('content', '#0d0d0d')
        }
      }
      win.activateLightMode = function () {
        document.documentElement.setAttribute('data-theme', 'light')
        if (document.querySelector('meta[name="theme-color"]') !== null) {
          document.querySelector('meta[name="theme-color"]').setAttribute('content', '#ffffff')
        }
      }
      const t = saveToLocal.get('theme')
    
          if (t === 'dark') activateDarkMode()
          else if (t === 'light') activateLightMode()
        
      const asideStatus = saveToLocal.get('aside-status')
      if (asideStatus !== undefined) {
        if (asideStatus === 'hide') {
          document.documentElement.classList.add('hide-aside')
        } else {
          document.documentElement.classList.remove('hide-aside')
        }
      }
    
    const detectApple = () => {
      if(/iPad|iPhone|iPod|Macintosh/.test(navigator.userAgent)){
        document.documentElement.classList.add('apple')
      }
    }
    detectApple()
    })(window)</script><link rel="stylesheet" href="/css/categories.css?v1"><link rel="stylesheet" href="/css/navigation.css?v1"><link rel="stylesheet" href="/css/custom.css?v1"><meta name="generator" content="Hexo 6.3.0"></head><body><div id="sidebar"><div id="menu-mask"></div><div id="sidebar-menus"><div class="avatar-img is-center"><img src="https://s2.loli.net/2022/09/22/36LN4kcjYOd9HT8.jpg" onerror="onerror=null;src='/img/friend_404.gif'" alt="avatar"/></div><div class="sidebar-site-data site-data is-center"><a href="/archives/"><div class="headline">文章</div><div class="length-num">43</div></a><a href="/tags/"><div class="headline">标签</div><div class="length-num">25</div></a><a href="/categories/"><div class="headline">分类</div><div class="length-num">11</div></a></div><hr/><div class="menus_items"><div class="menus_item"><a class="site-page" href="/"><i class="fa-fw fas fa-home"></i><span> 首页</span></a></div><div class="menus_item"><a class="site-page" href="/archives/"><i class="fa-fw fas fa-archive"></i><span> 时间轴</span></a></div><div class="menus_item"><a class="site-page" href="/tags/"><i class="fa-fw fas fa-tags"></i><span> 标签</span></a></div><div class="menus_item"><a class="site-page" href="/categories/"><i class="fa-fw fas fa-folder-open"></i><span> 分类</span></a></div></div></div></div><div class="post" id="body-wrap"><header class="not-top-img" id="page-header"><nav id="nav"><span id="blog_name"><a id="site-name" href="/">无人深空</a></span><div id="menus"></div><div class="menus_items"><div class="menus_item"><a class="site-page" href="/"><i class="fa-fw fas fa-home"></i><span> 首页</span></a></div><div class="menus_item"><a class="site-page" href="/archives/"><i class="fa-fw fas fa-archive"></i><span> 时间轴</span></a></div><div class="menus_item"><a class="site-page" href="/tags/"><i class="fa-fw fas fa-tags"></i><span> 标签</span></a></div><div class="menus_item"><a class="site-page" href="/categories/"><i class="fa-fw fas fa-folder-open"></i><span> 分类</span></a></div></div><div id="nav-right"><div id="search-button"><a class="site-page social-icon search"><i class="fas fa-search fa-fw"></i></a></div><div id="toggle-menu"><a class="site-page"><i class="fas fa-bars fa-fw"></i></a></div></div></nav></header><main class="layout" id="content-inner"><div id="post"><div id="post-info"><h1 class="post-title">JVM 番外-垃圾回收独立篇</h1><div id="post-meta"><div class="meta-firstline"><span class="post-meta-date"><i class="far fa-calendar-alt fa-fw post-meta-icon"></i><span class="post-meta-label">发表于</span><time class="post-meta-date-created" datetime="2022-10-07T00:29:46.000Z" title="发表于 2022-10-07 08:29:46">2022-10-07</time><span class="post-meta-separator">|</span><i class="fas fa-history fa-fw post-meta-icon"></i><span class="post-meta-label">更新于</span><time class="post-meta-date-updated" datetime="2022-10-08T06:28:49.712Z" title="更新于 2022-10-08 14:28:49">2022-10-08</time></span><span class="post-meta-categories"><span class="post-meta-separator">|</span><i class="fas fa-inbox fa-fw post-meta-icon"></i><a class="post-meta-categories" href="/categories/Java/">Java</a></span></div><div class="meta-secondline"><span class="post-meta-separator">|</span><span class="post-meta-wordcount"><i class="far fa-file-word fa-fw post-meta-icon"></i><span class="post-meta-label">字数总计:</span><span class="word-count">28.7k</span><span class="post-meta-separator">|</span><i class="far fa-clock fa-fw post-meta-icon"></i><span class="post-meta-label">阅读时长:</span><span>91分钟</span></span><span class="post-meta-separator">|</span><span class="post-meta-pv-cv" id="" data-flag-title="JVM 番外-垃圾回收独立篇"><i class="far fa-eye fa-fw post-meta-icon"></i><span class="post-meta-label">阅读量:</span><span id="busuanzi_value_page_pv"><i class="fa-solid fa-spinner fa-spin"></i></span></span></div></div></div><article class="post-content" id="article-container"><h2 id="JVM-上篇-垃圾回收"><a href="#JVM-上篇-垃圾回收" class="headerlink" title="JVM 上篇-垃圾回收"></a><a target="_blank" rel="noopener" href="https://www.bilibili.com/video/BV1PJ411n7xZ?p=134">JVM 上篇-垃圾回收</a></h2><blockquote>
<p>看 Java 不同版本新特性</p>
<ul>
<li>语法层面：Lambda 表达式、switch、自动装箱、自动拆箱、enum、&lt;&gt;……</li>
<li>API 层面：Stream API、新的日期时间、Optional、String、集合框架……</li>
<li>底层优化：JVM 的优化、GC 的变化、元空间、静态域、字符串常量池……</li>
</ul>
</blockquote>
<hr>
<h3 id="1-垃圾回收概述"><a href="#1-垃圾回收概述" class="headerlink" title="1.垃圾回收概述"></a>1.垃圾回收概述</h3><blockquote>
<p>垃圾收集，不是 Java 语言的伴生产物。早在 1960 年，第一门开始使用内存动态分配和垃圾收集技术的 Lisp 语言诞生</p>
<p>关于垃圾收集有三个经典问题：</p>
<ul>
<li>哪些内存需要回收？</li>
<li>什么时候回收？</li>
<li>如何回收？</li>
</ul>
<p><strong>垃圾收集机制是 Java 的招牌能力，极大地提高了开发效率</strong>。如今，垃圾收集几乎成为现代语言的标配，即使经过如此长时间的发展，Java 的垃圾收集机制仍然在不断的演进中，不同大小的设备、不同特征的应用场景，对垃圾收集提出了新的挑战，这当然也是面试的热点</p>
<p><strong>大厂面试题</strong></p>
<p><strong>蚂蚁金服</strong></p>
<ul>
<li><p>你知道哪几种垃圾回收器，各自的优缺点，重点讲一下 cms 和 G1？</p>
</li>
<li><p>JVM GC 算法有哪些，目前的 JDK 版本采用什么回收算法？</p>
</li>
<li><p>G1 回收器讲下回收过程 GC 是什么？为什么要有 GC？</p>
</li>
<li><p>GC 的两种判定方法？CMS 收集器与 G1 收集器的特点</p>
</li>
</ul>
<p><strong>百度</strong></p>
<ul>
<li><p>说一下 GC 算法，分代回收说下</p>
</li>
<li><p>垃圾收集策略和算法</p>
</li>
</ul>
<p><strong>天猫</strong></p>
<ul>
<li><p>JVM GC 原理，JVM 怎么回收内存</p>
</li>
<li><p>CMS 特点，垃圾回收算法有哪些？各自的优缺点，他们共同的缺点是什么？</p>
</li>
</ul>
<p><strong>滴滴</strong></p>
<ul>
<li>Java 的垃圾回收器都有哪些，说下 g1 的应用场景，平时你是如何搭配使用垃圾回收器的</li>
</ul>
<p><strong>京东</strong></p>
<ul>
<li><p>你知道哪几种垃圾收集器，各自的优缺点，重点讲下 cms 和 G1，</p>
</li>
<li><p>包括原理，流程，优缺点。垃圾回收算法的实现原理</p>
</li>
</ul>
<p><strong>阿里</strong></p>
<ul>
<li><p>讲一讲垃圾回收算法。</p>
</li>
<li><p>什么情况下触发垃圾回收？</p>
</li>
<li><p>如何选择合适的垃圾收集算法？</p>
</li>
<li><p>JVM 有哪三种垃圾回收器？</p>
</li>
</ul>
<p><strong>字节跳动</strong></p>
<ul>
<li>常见的垃圾回收器算法有哪些，各有什么优劣？</li>
<li><code>System.gc()</code> 和 <code>Runtime.gc()</code> 会做什么事情？</li>
<li>Java GC 机制？GC Roots 有哪些？</li>
<li>Java 对象的回收方式，回收算法。</li>
<li>CMS 和 G1 了解么，CMS 解决什么问题，说一下回收的过程。</li>
<li>CMS 回收停顿了几次，为什么要停顿两次?</li>
</ul>
</blockquote>
<h4 id="1-什么是垃圾"><a href="#1-什么是垃圾" class="headerlink" title="1.什么是垃圾"></a>1.什么是垃圾</h4><ol>
<li><p>垃圾是指在运行程序中没有任何指针指向的对象，这个对象就是需要被回收的垃圾</p>
<blockquote>
<p>如果不及时对内存中的垃圾进行清理，那么，这些垃圾对象所占的内存空间会一直保留到应用程序的结束，被保留的空间无法被其它对象使用，甚至可能导致内存溢出</p>
</blockquote>
</li>
<li><p>磁盘碎片整理的日子</p>
<blockquote>
<p>机械硬盘需要进行磁盘整理，同时还有坏道</p>
</blockquote>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/%E6%9C%BA%E6%A2%B0%E7%A1%AC%E7%9B%98.png" alt="机械硬盘"></p>
</li>
</ol>
<h4 id="2-为什么需要-GC"><a href="#2-为什么需要-GC" class="headerlink" title="2.为什么需要 GC"></a>2.为什么需要 GC</h4><blockquote>
<p>对于高级语言来说，一个基本认知是如果不进行垃圾回收，内存迟早都会被消耗完，因为不断地分配内存空间而不进行回收，就好像不停地生产生活垃圾而从来不打扫一样</p>
<p>除了释放没用的对象，垃圾回收也可以清除内存里的记录碎片。碎片整理将所占用的堆内存移到堆的一端，<strong>以便JVM 将整理出的内存分配给新的对象</strong></p>
<p>随着应用程序所应付的业务越来越庞大、复杂，用户越来越多，没有 GC 就不能保证应用程序的正常进行。而经常造成 STW 的 GC 又跟不上实际的需求，所以才会不断地尝试对 GC 进行优化</p>
</blockquote>
<h4 id="3-早期垃圾回收"><a href="#3-早期垃圾回收" class="headerlink" title="3.早期垃圾回收"></a>3.早期垃圾回收</h4><blockquote>
<p>在早期的 C&#x2F;C++时代，垃圾回收基本上是手工进行的。开发人员可以使用 new 关键字进行内存申请，并使用 delete 关键字进行内存释放。比如以下代码：</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line">MibBridge *pBridge= <span class="keyword">new</span> <span class="title class_">cmBaseGroupBridge</span>();</span><br><span class="line"><span class="comment">//如果注册失败，使用Delete释放该对象所占内存区域</span></span><br><span class="line"><span class="keyword">if</span> (pBridge-&gt;Register(kDestroy) != NO ERROR）</span><br><span class="line">	delete pBridge;</span><br></pre></td></tr></table></figure>

<p>这种方式可以灵活控制内存释放的时间，但是会给开发人员<strong>带来频繁申请和释放内存的管理负担</strong>。倘若有一处内存区间由于程序员编码的问题忘记被回收，那么就会产生<strong>内存泄漏，垃圾对象永远无法被清除</strong>，随着系统运行时间的不断增长，垃圾对象所耗内存可能持续上升，直到出现内存溢出并造成应用程序崩溃</p>
<p>在有了垃圾回收机制后，上述代码极有可能变成这样</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line">MibBridge *pBridge = <span class="keyword">new</span> <span class="title class_">cmBaseGroupBridge</span>();</span><br><span class="line">pBridge-&gt;Register(kDestroy);</span><br></pre></td></tr></table></figure>

<p>现在，除了 Java 以外，C#、Python、Ruby 等语言都使用了自动垃圾回收的思想，也是未来发展趋势，可以说这种自动化的内存分配和来及回收方式已经成为了现代开发语言必备的标准</p>
</blockquote>
<h4 id="4-Java-垃圾回收机制"><a href="#4-Java-垃圾回收机制" class="headerlink" title="4.Java 垃圾回收机制"></a>4.Java 垃圾回收机制</h4><ol>
<li><p><a target="_blank" rel="noopener" href="https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/toc.html">官方网站</a></p>
</li>
<li><p>自动内存管理，无需开发人员手动参与内存的分配与回收，这样降低内存泄漏和内存溢出的风险</p>
<blockquote>
<p>没有垃圾回收器，<code>java</code> 也会和 <code>cpp</code> 一样，各种悬垂指针，野指针，泄露问题让你头疼不已</p>
<p>自动内存管理机制，将程序员从繁重的内存管理中释放出来，可以更专心地专注于业务开发</p>
</blockquote>
</li>
<li><p>垃圾回收机制的担忧</p>
<blockquote>
<p>对于 Java 开发人员而言，自动内存管理就像是一个黑匣子，如果过度依赖于自动，那么这将会是一场灾难，最严重的就会<strong>弱化 Java 开发人员在程序出现内存溢出时定位问题和解决问题的能力</strong></p>
<p>此时，了解 JVM 的自动内存分配和内存回收原理就显得非常重要，只有在真正了解 JVM 是如何管理内存后，我们才能够在遇见 <code>OutofMemoryError</code> 时，快速地根据错误异常日志定位问题和解决问题</p>
<p>当需要排查各种内存溢出、内存泄漏问题时，当垃圾收集成为系统达到更高并发量的瓶颈时，我们就必须<strong>对这些自动化的技术实施必要的监控和调节</strong></p>
</blockquote>
</li>
<li><p>GC 主要关注的区域</p>
<blockquote>
<p>GC 主要关注于 <strong>方法区和堆</strong> 中的垃圾收集</p>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6.png" alt="垃圾回收"></p>
<p>垃圾收集器可以对年轻代回收，也可以对老年代回收，甚至是全栈和方法区的回收</p>
<p>其中，<strong>Java 堆是垃圾收集器的工作重点</strong></p>
<p>从次数上讲：</p>
<ul>
<li>频繁收集 Young 区</li>
<li>较少收集 Old 区</li>
<li>基本不收集 Perm 区（元空间）</li>
</ul>
</blockquote>
</li>
</ol>
<h3 id="2-垃圾回收相关算法"><a href="#2-垃圾回收相关算法" class="headerlink" title="2.垃圾回收相关算法"></a>2.垃圾回收相关算法</h3><ol>
<li><p>标记阶段</p>
<blockquote>
<p><strong>对象存活判断</strong></p>
<p>在堆里存放着几乎所有的 Java 对象实例，<strong>在 GC 执行垃圾回收之前，首先需要区分出内存中哪些是存活对象，哪些是已经死亡的对象</strong></p>
<p>只有被标记为己经死亡的对象，GC 才会在执行垃圾回收时，释放掉其所占用的内存空间，因此这个过程我们可以称为<strong>垃圾标记阶段</strong></p>
<p>那么在 JVM 中究竟是如何标记一个死亡对象呢？简单来说，<strong>当一个对象已经不再被任何的存活对象继续引用时，就可以宣判为已经死亡</strong></p>
<p>判断对象存活一般有两种方式：<strong>引用计数算法</strong>和<strong>可达性分析算法</strong></p>
</blockquote>
<ul>
<li><p>引用计数算法</p>
<blockquote>
<p>引用计数算法（Reference Counting）比较简单，<strong>对每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况</strong></p>
<p>对于一个对象 A，只要有任何一个对象引用了 A，则 A 的引用计数器就加 1；当引用失效时，引用计数器就减 1。只要对象 A 的引用计数器的值为 0，即表示对象 A 不可能再被使用，可进行回收</p>
<p>优点：</p>
<ul>
<li>实现简单，垃圾对象便于辨识；判定效率高，回收没有延迟性</li>
</ul>
<p>缺点：</p>
<ul>
<li>它需要单独的字段存储计数器，这样的做法<strong>增加了存储空间的开销</strong></li>
<li>每次赋值都需要更新计数器，伴随着加法和减法操作，这<strong>增加了时间开销</strong></li>
<li>引用计数器有一个严重的问题，即<strong>无法处理循环引用的情况</strong>。这是一条致命缺陷，导致在 Java 的垃圾回收器中没有使用这类算法</li>
</ul>
</blockquote>
<ul>
<li><p>循环引用</p>
<blockquote>
<p>当 p 的指针断开的时候，内部的引用形成一个循环，这就是循环引用</p>
</blockquote>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/%E5%BE%AA%E7%8E%AF%E5%BC%95%E7%94%A8.png" alt="循环引用"></p>
<blockquote>
<p>举例</p>
<p>测试 Java 中是否采用的是引用计数算法</p>
<p>下述进行了 GC 收集的行为，所以可以证明 JVM 中采用的不是引用计数器的算法</p>
<p><strong>（如果使用引用计数器，则无法回收，如下图所示）</strong></p>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/Java%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6.png" alt="Java垃圾回收"></p>
</blockquote>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RefCountGC</span> &#123;</span><br><span class="line">    <span class="comment">// 这个成员属性的唯一作用就是占用一点内存</span></span><br><span class="line">    <span class="keyword">private</span> <span class="type">byte</span>[] bigSize = <span class="keyword">new</span> <span class="title class_">byte</span>[<span class="number">5</span>*<span class="number">1024</span>*<span class="number">1024</span>];</span><br><span class="line">    <span class="comment">// 引用</span></span><br><span class="line">    <span class="type">Object</span> <span class="variable">reference</span> <span class="operator">=</span> <span class="literal">null</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">        <span class="type">RefCountGC</span> <span class="variable">obj1</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">RefCountGC</span>();</span><br><span class="line">        <span class="type">RefCountGC</span> <span class="variable">obj2</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">RefCountGC</span>();</span><br><span class="line"></span><br><span class="line">        obj1.reference = obj2;</span><br><span class="line">        obj2.reference = obj1;</span><br><span class="line"></span><br><span class="line">        obj1 = <span class="literal">null</span>;</span><br><span class="line">        obj2 = <span class="literal">null</span>;</span><br><span class="line">        <span class="comment">// 显示的执行垃圾收集行为</span></span><br><span class="line">        <span class="comment">// 这里发生GC，obj1和obj2是否被回收？</span></span><br><span class="line">        System.gc();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// 运行结果</span></span><br><span class="line">PSYoungGen: 15490K-&gt;808K(76288K)] 15490K-&gt;816K(251392K)</span><br></pre></td></tr></table></figure>
</li>
<li><p>引用计数算法小结</p>
<blockquote>
<p>引用计数算法，是很多语言的资源回收选择，例如因人工智能而更加火热的 Python，它更是同时支持引用计数和垃圾收集机制</p>
<p>具体哪种最优是要看场景的，业界有大规模实践中仅保留引用计数机制，以提高吞吐量的尝试</p>
<p>Java 并没有选择引用计数，是因为其存在一个基本的难题，也就是很难处理循环引用关系</p>
<p>Python 如何解决循环引用？</p>
<ul>
<li>手动解除：很好理解，就是在合适的时机，解除引用关系</li>
<li>使用弱引用 <code>weakref</code>，<code>weakref</code> 是 Python 提供的标准库，旨在解决循环引用</li>
</ul>
</blockquote>
</li>
</ul>
</li>
<li><p>可达性分析算法（根搜索算法、追踪性垃圾收集）</p>
<blockquote>
<p>相对于引用计数算法而言，可达性分析算法不仅同样具备实现简单和执行高效等特点，更重要的是该算法<strong>可以有效地解决在引用计数算法中循环引用的问题，防止内存泄漏的发生</strong></p>
<p>相较于引用计数算法，这里的可达性分析就是 <code>Java、C#</code> 选择的。这种类型的垃圾收集通常也叫作<strong>追踪性垃圾收集（Tracing Garbage Collection）</strong></p>
<p><strong>所谓 <code>GC Roots</code> 根集合就是一组必须活跃的引用</strong></p>
</blockquote>
<ul>
<li><p>基本思路</p>
<blockquote>
<p>可达性分析算法是<strong>以根对象集合（GC Roots）为起始点</strong>，按照从上至下的方式<strong>搜索被根对象集合所连接的目标对象是否可达</strong></p>
<p>使用可达性分析算法后，内存中的存活对象都会被根对象集合直接或间接连接着，<strong>搜索所走过的路径称为引用链（Reference Chain）</strong></p>
<p>如果目标对象没有任何引用链相连，则是不可达的，就意味着该对象己经死亡，可以标记为垃圾对象</p>
<p>在可达性分析算法中，只有能够被根对象集合直接或者间接连接的对象才是存活对象</p>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/GCRoots.png" alt="GCRoots"></p>
</blockquote>
</li>
<li><p>常见 <code>GC Roots</code></p>
<ul>
<li><p>虚拟机栈中引用的对象</p>
<blockquote>
<p>比如：各个线程被调用的方法中使用到的参数、局部变量等</p>
</blockquote>
</li>
<li><p>本地方法栈内 JNI（通常说的本地方法）引用的对象</p>
</li>
<li><p>方法区中类静态属性引用的对象</p>
<blockquote>
<p>比如：Java 类的引用类型静态变量</p>
</blockquote>
</li>
<li><p>方法区中常量引用的对象</p>
<blockquote>
<p>比如：字符串常量池（String Table）里的引用</p>
</blockquote>
</li>
<li><p>所有被同步锁 synchronized 持有的对象</p>
</li>
<li><p>Java 虚拟机内部的引用</p>
<blockquote>
<p>基本数据类型对应的 Class 对象，一些常驻的异常对象（如：<code>NullPointerException</code>、<code>OutOfMemoryError</code>），系统类加载器</p>
</blockquote>
</li>
<li><p>反映 java 虚拟机内部情况的 <code>JMXBean</code>、<code>JVMTI</code> 中注册的回调、本地代码缓存等</p>
</li>
</ul>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/%E5%B8%B8%E8%A7%81GCRoots.png" alt="常见GCRoots"></p>
<blockquote>
<p>除了这些固定的 GC Roots 集合以外，根据用户所选用的垃圾收集器以及当前回收的内存区域不同，还可以有其他对象<strong>临时性</strong>地加入，共同构成完整 GC Roots 集合。比如：分代收集和局部回收（<code>PartialGC</code>）</p>
<p>如果只针对 Java 堆中的某一块区域进行垃圾回收（比如：典型的只针对新生代），必须考虑到内存区域是虚拟机自己的实现细节，更不是孤立封闭的，这个区域的对象完全有可能被其他区域的对象所引用，这时候就需要一并将关联的区域对象也加入 <code>GCRoots</code> 集合中去考虑，才能保证可达性分析的准确性</p>
<p><strong>小技巧：由于 Root 采用栈方式存放变量和指针，所以如果一个指针，它保存了堆内存里面的对象，但是自己又不存放在堆内存里面，那它就是一个 Root</strong></p>
</blockquote>
</li>
<li><p>可达性分析注意事项</p>
<blockquote>
<p>如果要使用可达性分析算法来判断内存是否可回收，那么<strong>分析工作必须在一个能保障一致性的快照中进行</strong>。这点不满足的话分析结果的准确性就无法保证</p>
<p><strong>这点也是导致 GC 进行时必须 <code>Stop The World(STW)</code>的一个重要原因</strong></p>
<p>即使是号称（几乎）不会发生停顿的 CMS 收集器中，枚举根节点时也是必须要停顿的</p>
</blockquote>
</li>
</ul>
</li>
</ul>
</li>
<li><p>对象的 <code>finalization</code> 机制</p>
<blockquote>
<p>Java 语言提供了对象终止（finalization）机制来允许开发人员提供<strong>对象被销毁之前的自定义处理逻辑</strong></p>
<p>当垃圾回收器发现没有引用指向一个对象，即：<strong>垃圾回收此对象之前，总会先调用这个对象的 <code>finalize()</code> 方法</strong></p>
<p><code>finalize()</code> 方法允许在子类中被重写，<strong>用于在对象被回收时进行资源释放</strong>。通常在这个方法中进行一些资源释放和清理的工作，比如关闭文件、套接字和数据库连接等</p>
</blockquote>
<ul>
<li><p>永远不要主动调用某个对象的 <code>finalize()</code> 方法！应该交给垃圾回收机制调用。理由包括下面三点：</p>
<ul>
<li>在 <code>finalize()</code> 时可能会导致对象复活</li>
<li><code>finalize()</code> 方法的执行时间是没有保障的，它完全由 GC 线程决定，极端情况下，若不发生 GC，则 <code>finalize()</code> 方法将没有执行机会</li>
<li>一个糟糕的 <code>finalize()</code> 会严重影响 GC 的性能</li>
</ul>
</li>
</ul>
<blockquote>
<p>从功能上来说，<code>finalize()</code> 方法与 C++ 中的<code>析构函数</code>比较相似，但是 Java 采用的是基于垃圾回收器的自动内存管理机制，所以 <code>finalize()</code> 方法在本质上不同于 C++ 中的析构函数</p>
<p>如果从所有的根节点都无法访问到某个对象，说明对象己经不再使用了。一般来说，此对象需要被回收</p>
<p>但事实上，也并非是非死不可的，这时候它们暂时处于缓刑阶段。<strong>一个无法触及的对象有可能在某一个条件下复活自己</strong>，那么对它的回收就是不合理的，<strong>为此定义虚拟机中的对象可能的三种状态</strong></p>
<p><strong>只有在对象不可触及时才可以被回收</strong></p>
</blockquote>
<ul>
<li><p>由于 <code>finalize()</code> 方法的存在，虚拟机中的对象一般处于三种可能的状态：</p>
<ul>
<li><strong>可触及的</strong>：从根节点开始，可以到达这个对象</li>
<li><strong>可复活的</strong>：对象的所有引用都被释放，但是对象有可能在 <code>finalize()</code> 中复活</li>
<li><strong>不可触及的</strong>：对象的 <code>finalize()</code> 被调用，并且没有复活，那么就会进入不可触及状态。<strong>不可触及的对象不可能被复活，因为 <code>finalize()</code> 只会被调用一次</strong></li>
</ul>
</li>
<li><p>具体过程</p>
<blockquote>
<p>判定一个对象 <code>objA</code> 是否可回收，至少要经历两次标记过程</p>
</blockquote>
<ol>
<li>如果对象 <code>objA</code> 到 GC Roots 没有引用链，则进行第一次标记</li>
<li>进行筛选，判断此对象是否有必要执行 <code>finalize()</code> 方法</li>
<li>如果对象 <code>objA</code> 没有重写 <code>finalize()</code> 方法，或者 <code>finalize()</code> 方法已经被虚拟机调用过，则虚拟机视为没有必要执行，<code>objA</code> 被判定为不可触及的。</li>
<li>如果对象 <code>objA</code> 重写了 <code>finalize()</code> 方法，且还未执行过，那么 <code>objA</code> 会被插入到 F-Queue 队列中，<strong>由一个虚拟机自动创建的、低优先级的 <code>Finalizer线程</code>触发 <code>objA</code> 的 <code>finalize()</code> 方法并执行</strong></li>
<li><strong><code>finalize()</code> 方法是对象逃脱死亡的最后机会</strong>，稍后 GC 会对 F-Queue 队列中的对象进行第二次标记。如果 <code>objA</code> 在 <code>finalize()</code> 方法中与引用链上的任何一个对象建立了联系，那么在第二次标记时，<code>objA</code> 会被移出<code>即将回收</code>集合。之后对象会再次出现没有引用存在的情况。在这个情况下，<code>finalize()</code> 方法不会被再次调用，对象会直接变成不可触及的状态，也就是说，<strong>一个对象的 <code>finalize()</code> 方法只会被调用一次</strong></li>
</ol>
</li>
<li><p>举例</p>
<blockquote>
<p>在第一次 GC 时，执行了 <code>finalize()</code> 方法，但 <code>finalize()</code> 方法只会被调用一次，所以第二次该对象被 GC 标记并清除了</p>
</blockquote>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">CanReliveObj</span> &#123;</span><br><span class="line">    <span class="comment">// 类变量，属于GC Roots的一部分</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> CanReliveObj canReliveObj;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">protected</span> <span class="keyword">void</span> <span class="title function_">finalize</span><span class="params">()</span> <span class="keyword">throws</span> Throwable &#123;</span><br><span class="line">        <span class="built_in">super</span>.finalize();</span><br><span class="line">        System.out.println(<span class="string">&quot;调用当前类重写的finalize()方法&quot;</span>);</span><br><span class="line">        canReliveObj = <span class="built_in">this</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> InterruptedException &#123;</span><br><span class="line">        canReliveObj = <span class="keyword">new</span> <span class="title class_">CanReliveObj</span>();</span><br><span class="line">        canReliveObj = <span class="literal">null</span>;</span><br><span class="line">        System.gc();</span><br><span class="line">        System.out.println(<span class="string">&quot;-----------------第一次gc操作------------&quot;</span>);</span><br><span class="line">        <span class="comment">// 因为Finalizer线程的优先级比较低，暂停2秒，以等待它</span></span><br><span class="line">        Thread.sleep(<span class="number">2000</span>);</span><br><span class="line">        <span class="keyword">if</span> (canReliveObj == <span class="literal">null</span>) &#123;</span><br><span class="line">            System.out.println(<span class="string">&quot;obj is dead&quot;</span>);</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            System.out.println(<span class="string">&quot;obj is still alive&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        System.out.println(<span class="string">&quot;-----------------第二次gc操作------------&quot;</span>);</span><br><span class="line">        canReliveObj = <span class="literal">null</span>;</span><br><span class="line">        System.gc();</span><br><span class="line">        <span class="comment">// 下面代码和上面代码是一样的，但是 canReliveObj却自救失败了</span></span><br><span class="line">        Thread.sleep(<span class="number">2000</span>);</span><br><span class="line">        <span class="keyword">if</span> (canReliveObj == <span class="literal">null</span>) &#123;</span><br><span class="line">            System.out.println(<span class="string">&quot;obj is dead&quot;</span>);</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            System.out.println(<span class="string">&quot;obj is still alive&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// output</span></span><br><span class="line">-----------------第一次gc操作------------</span><br><span class="line">调用当前类重写的finalize()方法</span><br><span class="line">obj is still alive</span><br><span class="line">-----------------第二次gc操作------------</span><br><span class="line">obj is dead</span><br></pre></td></tr></table></figure></li>
</ul>
</li>
<li><p><code>MAT </code>与 <code>JProfiler</code> 的 GC Roots 溯源</p>
<ul>
<li><p>MAT 溯源</p>
<blockquote>
<p>MAT 是 Memory Analyzer 的简称，它是一款功能强大的 Java 堆内存分析器。用于查找内存泄漏以及查看内存消耗情况</p>
<p>MAT 是基于 Eclipse 开发的，<a href="%5Bhttp://www.eclipse.org/mat/%5D(http://www.eclipse.org/mat/)">是一款免费的性能分析工具</a></p>
<p>主要是获取 dump 文件，有以下三种方法</p>
<ul>
<li>命令行使用 <code>jmap</code></li>
<li>使用 <code>JVisualVM</code> 导出</li>
<li>使用 MAT 打开 Dump 文件</li>
</ul>
</blockquote>
</li>
<li><p><code>JProfiler</code> 溯源</p>
<blockquote>
<p>我们在实际的开发中，一般不会查找全部的 GC Roots，可能只是查找某个对象的整个链路，或者称为 GC Roots 溯源，这个时候，我们就可以使用 <code>JProfiler</code></p>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/JProfiler.png" alt="JProfiler"></p>
</blockquote>
</li>
</ul>
</li>
<li><p>清除阶段</p>
<blockquote>
<p>当成功区分出内存中存活对象和死亡对象后，GC 接下来的任务就是执行垃圾回收，释放掉无用对象所占用的内存空间，以便有足够的可用内存空间为新对象分配内存</p>
</blockquote>
<ul>
<li><p>标记-清除算法（Mark-Sweep）</p>
<blockquote>
<p>标记-清除算法（Mark-Sweep）是一种非常基础和常见的垃圾收集算法，该算法被 <code>J.McCarthy</code> 等人在 1960 年提出并并应用于 Lisp 语言</p>
<p>当堆中的有效内存空间（available memory）被耗尽的时候，就会停止整个程序（也被称为 stop the world），然后进行两项工作，第一项则是标记，第二项则是清除</p>
</blockquote>
<ul>
<li>标记：Collector 从引用根节点开始遍历，标记所有被引用的对象。<strong>一般是在对象的 Header 中记录为可达对象（标记的是可达对象！！！）</strong></li>
<li>清除：Collector 对堆内存从头到尾进行线性的遍历，如果发现某个对象在其 Header 中没有标记为可达对象，则将其回收</li>
</ul>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/%E6%A0%87%E8%AE%B0-%E6%B8%85%E9%99%A4%E7%AE%97%E6%B3%95.png" alt="标记-清除算法"></p>
<ul>
<li><p>优点</p>
<ul>
<li>易于理解和实现</li>
</ul>
</li>
<li><p>缺点</p>
<ul>
<li>标记清除算法的效率不算高</li>
<li>在进行 GC 的时候，需要停止整个应用程序，用户体验较差</li>
<li>这种方式清理出来的空闲内存是不连续的，产生内碎片，需要维护一个空闲列表</li>
</ul>
</li>
<li><p>何为清除？</p>
<blockquote>
<p>这里所谓的清除并不是真的置空，而是把需要清除的对象地址保存在空闲的地址列表里。下次有新对象需要加载时，判断垃圾的位置空间是否够，如果够就存放覆盖原有的地址</p>
</blockquote>
</li>
</ul>
</li>
<li><p>复制算法（Copying）</p>
<blockquote>
<p>为了解决标记-清除算法在垃圾收集效率方面的缺陷，<code>M.L.Minsky</code> 于 1963 年发表了著名的论文，<code>使用双存储区的 Lisp 语言垃圾收集器 CA LISP Garbage Collector Algorithm Using Serial Secondary Storage）</code></p>
<p><code>M.L.Minsky</code> 在该论文中描述的算法被人们称为复制（Copying）算法，它也被 <code>M.L.Minsky</code> 本人成功地引入到了 Lisp 语言的一个实现版本中</p>
</blockquote>
<ul>
<li><p>核心思想</p>
<blockquote>
<p>将活着的内存空间分为两块，每次只使用其中一块，在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中，之后清除正在使用的内存块中的所有对象，交换两个内存的角色，最后完成垃圾回收</p>
</blockquote>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/%E5%A4%8D%E5%88%B6%E7%AE%97%E6%B3%95.png" alt="复制算法"></p>
</li>
<li><p>优点</p>
<ul>
<li>没有标记和清除过程，实现简单，运行高效</li>
<li>复制过去以后保证空间的连续性，不会出现“碎片”问题。</li>
</ul>
</li>
<li><p>缺点</p>
<ul>
<li>此算法的缺点也是很明显的，就是需要两倍的内存空间。</li>
<li>对于 G1 这种分拆成为大量 region 的 GC，复制而不是移动，意味着 GC 需要维护 region 之间对象引用关系，不管是内存占用或者时间开销也不小</li>
</ul>
</li>
<li><p>注意事项</p>
<blockquote>
<p>如果系统中的垃圾对象很多，复制算法需要复制的存活对象数量并不会太大，或者说非常低才行</p>
</blockquote>
</li>
<li><p>应用场景</p>
<blockquote>
<p>在新生代，对常规应用的垃圾回收，一次通常可以回收 70% - 99% 的内存空间。回收性价比很高。所以现在的商业虚拟机都是用这种收集算法回收新生代</p>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/%E5%A4%8D%E5%88%B6%E7%AE%97%E6%B3%95%E9%80%82%E7%94%A8%E5%9C%BA%E6%99%AF.png" alt="复制算法适用场景"></p>
</blockquote>
</li>
</ul>
</li>
<li><p>标记-整理算法（或标记-压缩、Mark-Compact）</p>
<blockquote>
<p>复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生，但是在老年代，更常见的情况是大部分对象都是存活对象。如果依然使用复制算法，由于存活对象较多，复制的成本也将很高。因此基于老年代垃圾回收的特性，需要使用其他的算法。</p>
<p>标记一清除算法的确可以应用在老年代中，但是该算法不仅执行效率低下，而且在执行完内存回收后还会产生内存碎片，所以 JVM 的设计者需要在此基础之上进行改进。标记-压缩（Mark-Compact）算法由此诞生</p>
<p>1970 年前后，<code>G.L.Steele</code>、<code>C.J.Chene</code> 和 <code>D.s.Wise</code> 等研究者发布标记-压缩算法。在许多现代的垃圾收集器中，人们都使用了标记-压缩算法或其改进版本</p>
</blockquote>
<ul>
<li>标记：和标记清除算法一样，从根节点开始标记所有被引用对象</li>
<li>整理：将所有的存活对象压缩到内存的一端，按顺序排放。之后，清理边界外所有的空间</li>
</ul>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/%E6%A0%87%E8%AE%B0-%E6%95%B4%E7%90%86%E7%AE%97%E6%B3%95.png" alt="标记-整理算法"></p>
<ul>
<li><p>指针碰撞</p>
<blockquote>
<p>如果内存空间以规整和有序的方式分布，即已用和未用的内存都各自一边，彼此之间维系着一个记录下一次分配起始点的标记指针，当为新对象分配内存时，只需要通过修改指针的偏移量将新对象分配在第一个空闲内存位置上，这种分配方式就叫做指针碰撞（Bump tHe Pointer）</p>
</blockquote>
</li>
<li><p>优点</p>
<ul>
<li>消除了标记-清除算法当中，内存区域分散的缺点，我们需要给新对象分配内存时，JVM 只需要持有一个内存的起始地址即可</li>
<li>消除了复制算法当中，内存减半的高额代价</li>
</ul>
</li>
<li><p>缺点</p>
<ul>
<li>从效率上来说，标记-整理算法要低于复制算法</li>
<li>移动对象的同时，如果对象被其他对象引用，则还需要调整引用的地址</li>
<li>移动过程中，需要全程暂停用户应用程序。即：STW</li>
</ul>
</li>
<li><p>总结</p>
<blockquote>
<p>标记-压缩算法的最终效果等同于标记-清除算法执行完成后，再进行一次内存碎片整理，因此，也可以把它称为<strong>标记-清除-压缩（Mark-Sweep-Compact）算法</strong></p>
<p>二者的本质差异在于<strong>标记-清除算法是一种非移动式的回收算法，标记-压缩是移动式的</strong></p>
<p>是否移动回收后的存活对象是一项优缺点并存的风险决策。可以看到，标记的存活对象将会被整理，按照内存地址依次排列，而未被标记的内存会被清理掉。如此一来，当我们需要给新对象分配内存时，JVM 只需要持有一个内存的起始地址即可，这比维护一个空闲列表显然少了许多开销</p>
</blockquote>
</li>
</ul>
</li>
</ul>
</li>
<li><p>三种垃圾回收算法对比</p>
<table>
<thead>
<tr>
<th align="center"></th>
<th align="center">Mark-Sweep</th>
<th align="center">Mark-Compact</th>
<th align="center">Copying</th>
</tr>
</thead>
<tbody><tr>
<td align="center"><strong>速率</strong></td>
<td align="center">中等</td>
<td align="center">最慢</td>
<td align="center">最快</td>
</tr>
<tr>
<td align="center"><strong>空间开销</strong></td>
<td align="center">少（但会堆积碎片）</td>
<td align="center">少（不堆积碎片）</td>
<td align="center">通常需要活对象的 2 倍空间（不堆积碎片）</td>
</tr>
<tr>
<td align="center"><strong>移动对象</strong></td>
<td align="center">否</td>
<td align="center">是</td>
<td align="center">是</td>
</tr>
</tbody></table>
<blockquote>
<p>效率上来说，复制算法是当之无愧的老大，但是却浪费了太多内存</p>
<p>而为了尽量兼顾上面提到的三个指标，标记-整理算法相对来说更平滑一些，但是效率上不尽如人意，它比复制算法多了一个标记的阶段，比标记-清除多了一个整理内存的阶段</p>
<p>难道就没有一种最优算法吗？</p>
<p>回答：没有最好的算法，只有最合适的算法</p>
</blockquote>
</li>
<li><p>分代收集算法</p>
<blockquote>
<p>前面所有这些算法中，并没有一种算法可以完全替代其他算法，它们都具有自己独特的优势和特点。分代收集算法应运而生</p>
<p>分代收集算法，是基于这样一个事实：不同的对象的生命周期是不一样的。因此<strong>不同生命周期的对象可以采取不同的收集方式，以便提高回收效率</strong>。一般是把 Java 堆分为新生代和老年代，这样就可以根据各个年代的特点使用不同的回收算法，以提高垃圾回收的效率</p>
<p>在 Java 程序运行的过程中，会产生大量的对象，其中有些对象是与业务信息相关，比如<strong>Http 请求中的 Session 对象、线程、Socket 连接</strong>，这类对象跟业务直接挂钩，因此生命周期比较长。但是还有一些对象，主要是程序运行过程中生成的临时变量，这些对象生命周期会比较短，比如：<strong>String 对象</strong>，由于其不变类的特性，系统会产生大量的这些对象，有些对象甚至只用一次即可回收</p>
<p><strong>目前几乎所有的 GC 都采用分代收集算法执行垃圾回收的</strong></p>
<p>在 <code>HotSpot</code> 中，基于分代的概念，GC 所使用的内存回收算法必须结合年轻代和老年代各自的特点</p>
</blockquote>
<ul>
<li><p>新生代（Young Gen）</p>
<blockquote>
<p>年轻代特点：区域相对老年代较小，对象生命周期短、存活率低，回收频繁</p>
<p>这种情况<strong>复制算法</strong>的回收整理，速度是最快的。<strong>复制算法的效率只和当前存活对象大小有关，因此很适用于年轻代的回收</strong></p>
<p><strong>而复制算法内存利用率不高的问题，通过 hotspot 中的两个 survivor 的设计得到缓解</strong></p>
</blockquote>
</li>
<li><p>老年代（Tenured Gen）</p>
<blockquote>
<p>老年代特点：区域较大，对象生命周期长、存活率高，回收不及年轻代频繁</p>
<p>这种情况存在大量存活率高的对象，复制算法明显变得不合适。一般是由<strong>标记-清除</strong>或者是<strong>标记-清除与标记-整理的混合</strong>实现</p>
<ul>
<li>Mark 阶段的开销与存活对象的数量成正比</li>
<li>Sweep 阶段的开销与所管理区域的大小成正相关</li>
<li>Compact 阶段的开销与存活对象的数据成正比</li>
</ul>
<p>以 <code>HotSpot</code> 中的 CMS 回收器为例，CMS 是基于 Mark-Sweep 实现的，对于对象的回收效率很高</p>
<p>而对于碎片问题，CMS 采用基于 Mark-Compact 算法的 Serial Old 回收器作为补偿措施：当内存回收不佳（碎片导致的 Concurrent Mode Failure 时），将采用 Serial Old 执行 Full GC 以达到对老年代内存的整理</p>
<p>分代的思想被现有的虚拟机广泛使用。几乎所有的垃圾回收器都区分新生代和老年代</p>
</blockquote>
</li>
</ul>
</li>
<li><p>增量收集算法、分区算法</p>
<blockquote>
<p>增量收集算法、分区算法主要针对 STW 时间长的问题</p>
<p>没有提出新的具体算法，底层还是上述算法</p>
</blockquote>
<ul>
<li><p>增量收集算法</p>
<blockquote>
<p>上述现有的算法，在垃圾回收过程中，应用软件将处于一种 <strong>Stop the World</strong> 的状态</p>
<p>在 Stop the World 状态下，应用程序所有的线程都会挂起，暂停一切正常的工作，等待垃圾回收的完成。如果垃圾回收时间过长，应用程序会被挂起很久，将<strong>严重影响用户体验或者系统的稳定性</strong></p>
<p>为了解决这个问题，即对实时垃圾收集算法的研究直接导致了增量收集（Incremental Collecting）算法的诞生</p>
</blockquote>
<ul>
<li><p>基本思想</p>
<ul>
<li><p>如果一次性将所有的垃圾进行处理，需要造成系统长时间的停顿，那么就可以让垃圾收集线程和应用程序线程交替执行</p>
</li>
<li><p><strong>垃圾收集线程只收集一小片区域的内存空间，接着切换到应用程序线程。依次反复，直到垃圾收集完成</strong></p>
</li>
<li><p>总的来说，增量收集算法的基础仍是传统的标记-清除和复制算法。增量收集算法通过<strong>对线程间冲突的妥善处理，允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作</strong></p>
</li>
</ul>
</li>
<li><p>优点</p>
<ul>
<li>使用这种方式，由于在垃圾回收过程中，间断性地还执行了应用程序代码，所以能减少系统的停顿时间</li>
</ul>
</li>
<li><p>缺点</p>
<ul>
<li>因为线程切换和上下文转换的消耗，会使得垃圾回收的总体成本上升，造成系统吞吐量的下降</li>
</ul>
</li>
</ul>
</li>
<li><p>分区算法</p>
<blockquote>
<p>一般来说，在相同条件下，堆空间越大，一次 GC 时所需要的时间就越长，有关 GC 产生的停顿也越长</p>
<p>为了更好地控制 GC 产生的停顿时间，将一块大的内存区域分割成多个小块，根据目标的停顿时间，每次合理地回收若干个小区间，而不是整个堆空间，从而减少一次 GC 所产生的停顿</p>
<p><strong>分代算法将按照对象的生命周期长短划分成两个部分，分区算法将整个堆空间划分成连续的不同小区间</strong></p>
<p>每一个小区间都独立使用，独立回收。这种算法的好处是可以控制一次回收多少个小区间</p>
</blockquote>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/%E5%88%86%E5%8C%BA%E7%AE%97%E6%B3%95.png" alt="分区算法"></p>
</li>
</ul>
</li>
<li><p>总结</p>
<blockquote>
<p>注意，这些只是基本的算法思路，实际 GC 实现过程要复杂的多</p>
<p>目前还在发展中的前沿 GC 都是复合算法，并且并行和并发兼备</p>
</blockquote>
</li>
</ol>
<h3 id="3-垃圾回收相关概念"><a href="#3-垃圾回收相关概念" class="headerlink" title="3.垃圾回收相关概念"></a>3.垃圾回收相关概念</h3><h4 id="1-System-gc-的理解"><a href="#1-System-gc-的理解" class="headerlink" title="1.System.gc() 的理解"></a>1.System.gc() 的理解</h4><ul>
<li><p>在默认情况下，通过 <code>system.gc()</code> 或者 <code>Runtime.getRuntime().gc()</code> 的调用，<strong>会显式触发 Full GC</strong>，同时对老年代和新生代进行回收，尝试释放被丢弃对象占用的内存</p>
<blockquote>
<p><code>System.gc()</code> 在内部调用 <code>Runtime.gc()</code></p>
<p>硬要说区别的话 <code>Runtime.gc()</code> 是 <code>native method</code></p>
<p> <img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/%E4%B8%A4%E4%B8%AAgc%E7%9A%84%E5%8C%BA%E5%88%AB.png" alt="两个gc的区别"></p>
</blockquote>
</li>
<li><p>然而 <code>System.gc()</code> 调用附带一个免责声明，无法保证对垃圾收集器的调用(不能确保立即生效)</p>
</li>
<li><p>JVM 实现者可以通过 <code>System.gc() </code>调用来决定 JVM 的 GC 行为</p>
<blockquote>
<p>而一般情况下，垃圾回收应该是自动进行的，无须手动触发，否则就太过于麻烦了。在一些特殊情况下，如正在编写一个性能基准时，可以在运行之间调用 <code>System.gc()</code></p>
</blockquote>
</li>
<li><p>举例1</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">SystemGCTest</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">        <span class="keyword">new</span> <span class="title class_">SystemGCTest</span>();</span><br><span class="line">        System.gc();<span class="comment">// 提醒JVM的垃圾回收器执行gc，但是不确定是否马上执行gc</span></span><br><span class="line">        <span class="comment">// 与Runtime.getRuntime().gc();的作用一样</span></span><br><span class="line"></span><br><span class="line">        System.runFinalization();<span class="comment">//强制执行使用引用的对象的finalize()方法</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">protected</span> <span class="keyword">void</span> <span class="title function_">finalize</span><span class="params">()</span> <span class="keyword">throws</span> Throwable &#123;</span><br><span class="line">        <span class="built_in">super</span>.finalize();</span><br><span class="line">        System.out.println(<span class="string">&quot;SystemGCTest 重写了 finalize()&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
</li>
<li><p>举例2</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Test</span> &#123;</span><br><span class="line">    <span class="comment">// buffer 不会被回收</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">TestGC1</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="type">byte</span>[] buffer = <span class="keyword">new</span> <span class="title class_">byte</span>[<span class="number">10</span> * <span class="number">1024</span> * <span class="number">1024</span>];</span><br><span class="line">        System.gc();</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// buffer 会被回收</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">TestGC2</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="type">byte</span>[] buffer = <span class="keyword">new</span> <span class="title class_">byte</span>[<span class="number">10</span> * <span class="number">1024</span> * <span class="number">1024</span>];</span><br><span class="line">        buffer = <span class="literal">null</span>;</span><br><span class="line">        System.gc();</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// buffer 不会被回收</span></span><br><span class="line">    <span class="comment">// 由于 slot 复用的存在，buffer 在生命周期外仍然占据着一个 slot</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">TestGC3</span><span class="params">()</span> &#123;</span><br><span class="line">        &#123;</span><br><span class="line">            <span class="type">byte</span>[] buffer = <span class="keyword">new</span> <span class="title class_">byte</span>[<span class="number">10</span> * <span class="number">1024</span> * <span class="number">1024</span>];</span><br><span class="line">        &#125;</span><br><span class="line">        System.gc();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// buffer 会被回收</span></span><br><span class="line">    <span class="comment">// 虽然存在 slot 复用，但是 buffer 在结束生命周期后 slot 被 val 占用</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">TestGC4</span><span class="params">()</span> &#123;</span><br><span class="line">        &#123;</span><br><span class="line">            <span class="type">byte</span>[] buffer = <span class="keyword">new</span> <span class="title class_">byte</span>[<span class="number">10</span> * <span class="number">1024</span> * <span class="number">1024</span>];</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="type">int</span> <span class="variable">val</span> <span class="operator">=</span> <span class="number">10</span>;</span><br><span class="line">        System.gc();</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">        <span class="type">Test</span> <span class="variable">test</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Test</span>();</span><br><span class="line">        test.TestGC1();</span><br><span class="line">        test.TestGC2();</span><br><span class="line">        test.TestGC3();</span><br><span class="line">        test.TestGC4();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li>
</ul>
<h4 id="2-内存溢出与内存泄漏"><a href="#2-内存溢出与内存泄漏" class="headerlink" title="2.内存溢出与内存泄漏"></a>2.内存溢出与内存泄漏</h4><ul>
<li><p>内存溢出（OOM）</p>
<blockquote>
<p>内存溢出相对于内存泄漏来说，尽管更容易被理解，但是同样的，内存溢出也是引发程序崩溃的罪魁祸首之一</p>
<p>由于 GC 一直在发展，所有一般情况下，除非应用程序占用的内存增长速度非常快，造成垃圾回收已经跟不上内存消耗的速度，否则不太容易出现 OOM 的情况</p>
<p>大多数情况下，GC 会进行各种年龄段的垃圾回收，实在不行了就放大招，来一次独占式的 Full GC 操作，这时候会回收大量的内存，供应用程序继续使用</p>
<p><code>javadoc</code> 中对 <code>OutOfMemoryError</code> 的解释是：<strong>没有空闲内存，并且垃圾收集器也无法提供更多内存</strong></p>
</blockquote>
<ul>
<li><p>Java 虚拟机堆内存不够，原因有二</p>
<ul>
<li><p>Java 虚拟机的堆内存设置不够</p>
<blockquote>
<p>可能存在内存泄漏问题</p>
<p>比如我们要处理比较可观的数据量，但是没有显式指定 JVM 堆大小或者指定数值偏小。可以通过参数 <code>-Xms</code> 、<code>-Xmx</code> 来调整</p>
</blockquote>
</li>
<li><p>代码中创建了大量大对象，并且长时间不能被垃圾收集器收集（存在被引用）</p>
<blockquote>
<p>对于老版本的 Oracle JDK，因为永久代的大小是有限的，并且 JVM 对永久代垃圾回收（如，常量池回收、卸载不再需要的类型）非常不积极，所以当我们不断添加新类型的时候，永久代出现 <code>OutOfMemoryError</code> 也非常多见，尤其是在运行时存在大量动态类型生成的场合</p>
<p>类似 <code>intern</code> 字符串缓存占用太多空间，也会导致 OOM 问题。对应的异常信息，会标记出来和永久代相关：<code>java.lang.OutOfMemoryError: PermGen space</code></p>
<p>随着元数据区的引入，方法区内存已经不再那么窘迫，所以相应的 OOM 有所改观，出现 OOM，异常信息则变成了：<code>java.lang.OutofMemoryError:Metaspace</code></p>
<p>直接内存不足，也会导致 OOM</p>
</blockquote>
</li>
</ul>
</li>
<li><p>这里面隐含着一层意思是，在抛出 <code>OutOfMemoryError</code> 之前，通常垃圾收集器会被触发，尽其所能去清理出空间</p>
<ul>
<li>例如：在引用机制分析中，涉及到 JVM 会去尝试回收软引用指向的对象等</li>
<li><code>java.nio.BIts.reserveMemory()</code> 方法中，我们能清楚的看到，<code>System.gc()</code> 会被调用，以清理空间</li>
</ul>
</li>
<li><p>当然，也不是在任何情况下垃圾收集器都会被触发的</p>
<blockquote>
<p>比如分配一个超大对象，类似一个超大数组超过堆的最大值，JVM 可以判断出垃圾收集并不能解决这个问题，所以直接抛出 <code>OutOfMemoryError</code></p>
</blockquote>
</li>
</ul>
</li>
<li><p>内存泄露（Memory Leak）</p>
<blockquote>
<p>也称作<code>存储渗漏</code></p>
<p>严格来说，<strong>只有对象不会再被程序用到了，但是 GC 又不能回收他们的情况，才叫内存泄漏</strong></p>
<p><strong>但实际情况很多时候一些不太好的实践（或疏忽）会导致对象的生命周期变得很长甚至导致 OOM，也可以叫做宽泛意义上的<code>内存泄漏</code></strong></p>
<p>尽管内存泄漏并不会立刻引起程序崩溃，但是一旦发生内存泄漏，程序中的可用内存就会被逐步蚕食，直至耗尽所有内存，最终出现 <code>OutOfMemory</code> 异常，导致程序崩溃</p>
<p>注意，<strong>这里的存储空间并不是指物理内存，而是指虚拟内存大小，这个虚拟内存大小取决于磁盘交换区设定的大小</strong></p>
</blockquote>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F.png" alt="内存泄漏"></p>
<ul>
<li><p>可能造成内存泄漏的情况举例</p>
<ul>
<li><p>单例模式</p>
<blockquote>
<p><strong>单例的生命周期和应用程序是一样长的</strong>，所以单例程序中，如果持有对外部对象的引用的话，那么这个外部对象是不能被回收的，则会导致内存泄漏的产生</p>
</blockquote>
</li>
<li><p>一些提供 close 的资源未关闭导致内存泄漏</p>
<blockquote>
<p>数据库连接 <code>dataSourse.getConnection()</code></p>
<p>网络连接 <code>socket</code> 和 <code>IO</code> 连接必须手动 <code>close</code></p>
<p>否则是不能被回收的</p>
</blockquote>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<h4 id="3-Stop-The-World"><a href="#3-Stop-The-World" class="headerlink" title="3.Stop The World"></a>3.Stop The World</h4><ul>
<li><p>Stop-the-World，简称 STW，指的是 GC 事件发生过程中，会产生应用程序的停顿。<strong>停顿产生时整个应用程序线程都会被暂停，没有任何响应，有点像卡死的感觉，这个停顿称为 STW</strong></p>
</li>
<li><p>可达性分析算法中枚举根节点（GC Roots）会导致所有 Java 执行线程停顿</p>
<ul>
<li>分析工作必须在一个能确保一致性的快照中进行</li>
<li>一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上</li>
<li><strong>如果出现分析过程中对象引用关系还在不断变化，则分析结果的准确性无法保证</strong></li>
</ul>
</li>
<li><p>被 STW 中断的应用程序线程会在完成 GC 之后恢复，频繁中断会让用户感觉像是网速不快造成电影卡带一样，所以我们需要减少 STW 的发生</p>
</li>
<li><p>STW 事件和采用哪款 GC 无关，所有的 GC 都有这个事件</p>
<blockquote>
<p>哪怕是 G1 也不能完全避免 Stop-the-World 情况发生，只能说垃圾回收器越来越优秀，回收效率越来越高，尽可能地缩短了暂停时间</p>
</blockquote>
</li>
<li><p><strong>STW 是 JVM 在后台自动发起和自动完成的</strong>。在用户不可见的情况下，把用户正常的工作线程全部停掉</p>
<blockquote>
<p>开发中不要用 <code>System.gc()</code>，会导致 Stop-the-World 的发生</p>
</blockquote>
</li>
</ul>
<h4 id="4-垃圾回收的并行（Parallel）与并发（Concurrent）"><a href="#4-垃圾回收的并行（Parallel）与并发（Concurrent）" class="headerlink" title="4.垃圾回收的并行（Parallel）与并发（Concurrent）"></a><strong>4.垃圾回收的并行（Parallel）与并发（Concurrent）</strong></h4><ol>
<li><p>并发与并行的理解</p>
<blockquote>
<p>并发，指的是多个事情，在<strong>同一时间段内</strong>同时发生了</p>
<p>并行，指的是多个事情，在<strong>同一时间点上</strong>同时发生了</p>
<p>并发的多个任务之间是互相抢占资源的</p>
<p>并行的多个任务之间是不互相抢占资源的</p>
<p>只有在多 CPU 或者一个 CPU 多核的情况中，才会发生并行</p>
<p>否则，看似同时发生的事情，其实都是并发执行的</p>
</blockquote>
</li>
<li><p>垃圾回收器的并行、串行、并发</p>
<blockquote>
<p>并发和串行、并行，在谈论垃圾收集器的上下文语境中，它们可以解释如下</p>
</blockquote>
<ul>
<li><p>并行</p>
<blockquote>
<p>指多条垃圾收集线程并行工作，但此时用户线程仍处于等待状态</p>
<p>如 <code>ParNew、Parallel Scavenge、Parallel Old</code></p>
</blockquote>
</li>
<li><p>串行</p>
<blockquote>
<p>相较于并行的概念，垃圾回收工作单线程执行</p>
<p>如果内存不够，则程序暂停，启动 JM 垃圾回收器进行垃圾回收。回收完，再启动程序的线程</p>
</blockquote>
</li>
<li><p>并发</p>
<blockquote>
<p><strong>指用户线程与垃圾收集线程同时执行（但不一定是并行的，可能会交替执行）</strong></p>
<p>垃圾回收线程在执行时不会停顿用户程序的运行</p>
<p>用户程序在继续运行，而垃圾收集程序线程运行于另一个 CPU 上</p>
<p>如：<code>CMS、G1</code></p>
</blockquote>
</li>
</ul>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/%E4%B8%B2%E5%B9%B6%E8%A1%8C.png" alt="串并行"></p>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/%E5%B9%B6%E5%8F%91.png" alt="并发"></p>
</li>
</ol>
<h4 id="5-安全点与安全区域"><a href="#5-安全点与安全区域" class="headerlink" title="5.安全点与安全区域"></a>5.安全点与安全区域</h4><ol>
<li><p>安全点（Safe Point）</p>
<blockquote>
<p>程序执行时并非在所有地方都能停顿下来开始 GC，只有在特定的位置才能停顿下来开始 GC，这些位置称为<code>安全点</code></p>
<p>Safe Point 的选择很重要，<strong>如果太少可能导致 GC 等待的时间太长，如果太频繁可能导致运行时的性能问题</strong></p>
<p>大部分指令的执行时间都非常短暂，通常会根据<strong>是否具有让程序长时间执行的特征</strong>为标准</p>
<p>选择一些执行时间较长的指令作为 Safe Point，如方法调用、循环跳转和异常跳转等</p>
</blockquote>
<ul>
<li><p>如何在 GC 发生时，检查所有线程都跑到最近的安全点停顿下来</p>
<ul>
<li><p>抢先式中断：（目前没有虚拟机采用了）</p>
<blockquote>
<p>首先中断所有线程。如果还有线程不在安全点，就恢复线程，让线程跑到安全点</p>
</blockquote>
</li>
<li><p>主动式中断</p>
<blockquote>
<p>设置一个中断标志，各个线程运行到 Safe Point 的时候主动轮询这个标志，如果中断标志为真，则将自己进行中断挂起（有轮询的机制）</p>
</blockquote>
</li>
</ul>
</li>
</ul>
</li>
<li><p>安全区域（Safe Region）</p>
<blockquote>
<p>安全点机制保证了程序执行时，在不太长的时间内就会遇到可进入 GC 的 <code>SafePoint</code></p>
<p>但是，程序不执行的时候呢？</p>
<p>例如线程处于 Sleep 状态或 Blocked 状态，这时候线程无法响应 JVM 的中断请求，走到安全点去中断挂起，JVM 也不太可能等待线程被唤醒</p>
<p>对于这种情况，就需要安全区域（Safe Region）来解决</p>
<p><strong>安全区域是指在一段代码片段中，对象的引用关系不会发生变化，在这个区域中的任何位置开始 GC 都是安全的</strong>（也可以把 Safe Region 看做是被扩展了的 Safe Point）</p>
</blockquote>
<ul>
<li>当线程运行到 Safe Region 的代码时，首先标识已经进入了 Safe Region，如果这段时间内发生 GC，JVM 会忽略标识为 Safe Region 状态的线程</li>
<li>当线程即将离开 Safe Region 时，会检查 JVM 是否已经完成 GC，如果完成了则继续运行，否则线程必须等待直到收到可以安全离开 Safe Region 的信号为止</li>
</ul>
</li>
</ol>
<h4 id="6-四大引用（面试重点）"><a href="#6-四大引用（面试重点）" class="headerlink" title="6.四大引用（面试重点）"></a>6.四大引用（面试重点）</h4><blockquote>
<p>在 JDK1.2 版之后，Java 对引用的概念进行了扩充，将引用分为：强引用（Strong Reference）、软引用（Soft Reference）、弱引用（Weak Reference）、虚引用（Phantom Reference）这 4 种引用强度依次逐渐减弱</p>
<p>除强引用外，其他 3 种引用均可以在 <code>java.lang.ref</code> 包中找到它们的身影</p>
</blockquote>
<ul>
<li><p>如下图，显示了这 3 种引用类型对应的类，开发人员可以在应用程序中直接使用它们</p>
<p> <img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/%E5%9B%9B%E5%A4%A7%E5%BC%95%E7%94%A8.png" alt="四大引用"></p>
</li>
<li><p>Reference 子类中只有终结器引用是包内可见的，其他 3 种引用类型均为 public，可以在应用程序中直接使用</p>
</li>
<li><p>强引用 <code>StrongReference</code></p>
<blockquote>
<p>最传统的引用的定义，是指在程序代码之中普遍存在的引用赋值</p>
<p>如  <code>Object obj = new Object()</code> </p>
<p>无论任何情况下，只要强引用关系还存在，垃圾收集器就永远不会回收掉被引用的对象</p>
</blockquote>
</li>
<li><p>软引用 <code>SoftReference</code></p>
<blockquote>
<p>在系统将要发生内存溢出之前，将会把这些对象列入回收范围之中进行第二次回收</p>
<p>如果这次回收后还没有足够的内存，才会抛出内存流出异常</p>
</blockquote>
</li>
<li><p>弱引用 <code>WeakReference</code></p>
<blockquote>
<p>被弱引用关联的对象只能生存到下一次垃圾收集之前</p>
<p>当垃圾收集器工作时，无论内存空间是否足够，都会回收掉被弱引用关联的对象</p>
</blockquote>
</li>
<li><p>虚引用 <code>PhantomReference</code></p>
<blockquote>
<p>一个对象是否有虚引用的存在，完全不会对其生存时间构成影响，也无法通过虚引用来获得一个对象的实例</p>
<p>为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知</p>
</blockquote>
</li>
</ul>
<h4 id="7-强引用（Strong-Reference）——不回收"><a href="#7-强引用（Strong-Reference）——不回收" class="headerlink" title="7.强引用（Strong Reference）——不回收"></a>7.强引用（Strong Reference）——不回收</h4><ul>
<li><p>在 Java 程序中，最常见的引用类型是强引用（<strong>普通系统 99%以上都是强引用</strong>），也就是我们最常见的普通对象引用，也是默认的引用类型</p>
</li>
<li><p>当在 Java 语言中使用 new 操作符创建一个新的对象，并将其赋值给一个变量的时候，这个变量就成为指向该对象的一个强引用</p>
</li>
<li><p><strong>强引用的对象是可触及的，垃圾收集器就永远不会回收掉被引用的对象</strong></p>
</li>
<li><p>对于一个普通的对象，如果没有其他的引用关系，只要超过了引用的作用域或者显式地将相应（强）引用赋值为 null，就是可以当做垃圾被收集了，当然具体回收时机还是要看垃圾收集策略</p>
</li>
<li><p><strong>相对的，软引用、弱引用和虚引用的对象是软可触及、弱可触及和虚可触及的</strong>，在一定条件下，都是可以被回收的。<strong>所以强引用是造成 Java 内存泄漏的主要原因之一</strong></p>
</li>
<li><p>强引用举例</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="type">StringBuffer</span> <span class="variable">str</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">StringBuffer</span>(<span class="string">&quot;hello mogublog&quot;</span>);</span><br><span class="line"><span class="type">StringBuffer</span> <span class="variable">str1</span> <span class="operator">=</span> str;</span><br></pre></td></tr></table></figure>

<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/%E5%BC%BA%E5%BC%95%E7%94%A8%E5%86%85%E5%AD%98%E7%BB%93%E6%9E%84.png" alt="强引用内存结构"></p>
</li>
<li><p>强引用具备以下特点：</p>
<ul>
<li>强引用可以直接访问目标对象</li>
<li>强引用所指向的对象在任何时候都不会被系统回收，虚拟机宁愿抛出 OOM 异常，也不会回收强引用所指向对象</li>
<li>强引用可能导致内存泄漏</li>
</ul>
</li>
</ul>
<h4 id="8-软引用（Soft-Reference）——内存不足即回收"><a href="#8-软引用（Soft-Reference）——内存不足即回收" class="headerlink" title="8.软引用（Soft Reference）——内存不足即回收"></a>8.软引用（Soft Reference）——内存不足即回收</h4><ul>
<li><p>软引用是用来描述一些还有用，但非必需的对象。<strong>只被软引用关联着的对象，在系统将要发生内存溢出异常前，会把这些对象列进回收范围之中进行第二次回收</strong>，如果这次回收还没有足够的内存，才会抛出内存溢出异常</p>
</li>
<li><p><strong>软引用通常用来实现内存敏感的缓存。比如：高速缓存就有用到软引用。如果还有空闲内存，就可以暂时保留缓存，当内存不足时清理掉，这样就保证了使用缓存的同时，不会耗尽内存</strong></p>
</li>
<li><p>垃圾回收器在某个时刻决定回收软可达的对象的时候，会清理软引用，并可选地把引用存放到一个引用队列（Reference Queue）</p>
</li>
<li><p>类似弱引用，只不过 Java 虚拟机会尽量让软引用的存活时间长一些，迫不得已才清理</p>
</li>
<li><p>在 JDK1.2 版之后提供了 <code>java.lang.ref.SoftReference</code> 类来实现软引用</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="type">Object</span> <span class="variable">obj</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Object</span>();	<span class="comment">// 声明强引用</span></span><br><span class="line">SoftReference&lt;Object&gt; sf = <span class="keyword">new</span> <span class="title class_">SoftReference</span>&lt;&gt;(obj);</span><br><span class="line">obj = <span class="literal">null</span>; 				<span class="comment">// 销毁强引用</span></span><br></pre></td></tr></table></figure></li>
</ul>
<h4 id="9-弱引用（Weak-Reference）——发现即回收"><a href="#9-弱引用（Weak-Reference）——发现即回收" class="headerlink" title="9.弱引用（Weak Reference）——发现即回收"></a>9.弱引用（Weak Reference）——发现即回收</h4><ul>
<li><p>弱引用也是用来描述那些非必需对象，<strong>只被弱引用关联的对象只能生存到下一次垃圾收集发生为止</strong>。在系统 GC 时，只要发现弱引用，不管系统堆空间使用是否充足，都会回收掉只被弱引用关联的对</p>
</li>
<li><p>但是由于垃圾回收器的线程通常优先级很低，因此并不一定能很快地发现持有弱引用的对象。在这种情况下，弱引用对象可以存在较长的时间</p>
</li>
<li><p>弱引用和软引用一样，在构造弱引用时，也可以指定一个引用队列，当弱引用对象被回收时，就会加入指定的引用队列，通过这个队列可以跟踪对象的回收情况</p>
</li>
<li><p><strong>软引用、弱引用都非常适合来保存那些可有可无的缓存数据</strong>。如果这么做，当系统内存不足时，这些缓存数据会被回收，不会导致内存溢出。而当内存资源充足时，这些缓存数据又可以存在相当长的时间，从而起到加速系统的作用</p>
</li>
<li><p>在 JDK1.2 版之后提供了 <code>WeakReference</code> 类来实现弱引用</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="type">Object</span> <span class="variable">obj</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Object</span>();	<span class="comment">// 声明强引用</span></span><br><span class="line">WeakReference&lt;Object&gt; sf = <span class="keyword">new</span> <span class="title class_">WeakReference</span>&lt;&gt;(obj);</span><br><span class="line">obj = <span class="literal">null</span>;					<span class="comment">// 销毁强引用</span></span><br></pre></td></tr></table></figure>
</li>
<li><p>弱引用对象与软引用对象的最大不同就在于，当 GC 在进行回收时，需要通过算法检查是否回收软引用对象，而对于弱引用对象，GC 总是进行回收。<strong>弱引用对象更容易、更快被 GC 回收</strong></p>
</li>
<li><p><strong>面试题：你开发中使用过 <code>WeakHashMap</code> 吗？</strong></p>
<blockquote>
<p><code>WeakHashMap</code> 用来存储图片信息，可以在内存不足的时候及时回收，避免了 OOM</p>
</blockquote>
</li>
</ul>
<h4 id="10-虚引用（Phantom-Reference）——对象回收跟踪"><a href="#10-虚引用（Phantom-Reference）——对象回收跟踪" class="headerlink" title="10.虚引用（Phantom Reference）——对象回收跟踪"></a>10.虚引用（Phantom Reference）——对象回收跟踪</h4><ul>
<li><p>也称为<code>幽灵引用</code>或者<code>幻影引用</code>，是所有引用类型中最弱的一个</p>
</li>
<li><p>一个对象是否有虚引用的存在，完全不会决定对象的生命周期。如果一个对象仅持有虚引用，那么它和没有引用几乎是一样的，随时都可能被垃圾回收器回收</p>
</li>
<li><p>它不能单独使用，也无法通过虚引用来获取被引用的对象。当试图通过虚引用的 <code>get()</code> 方法取得对象时，总是 null</p>
</li>
<li><p><strong>为一个对象设置虚引用关联的唯一目的在于跟踪垃圾回收过程。比如：能在这个对象被收集器回收时收到一个系统通知</strong></p>
</li>
<li><p>虚引用必须和引用队列一起使用。虚引用在创建时必须提供一个引用队列作为参数。当垃圾回收器准备回收一个对象时，如果发现它还有虚引用，就会在回收对象后，将这个虚引用加入引用队列，以通知应用程序对象的回收情况</p>
</li>
<li><p>也称为“幽灵引用”或者“幻影引用”，是所有引用类型中最弱的一个</p>
</li>
<li><p>一个对象是否有虚引用的存在，完全不会决定对象的生命周期。如果一个对象仅持有虚引用，那么它和没有引用几乎是一样的，随时都可能被垃圾回收器回收。</p>
</li>
<li><p>它不能单独使用，也无法通过虚引用来获取被引用的对象。当试图通过虚引用的 get()方法取得对象时，总是 null</p>
</li>
<li><p><strong>为一个对象设置虚引用关联的唯一目的在于跟踪垃圾回收过程</strong></p>
<blockquote>
<p>比如：能在这个对象被收集器回收时收到一个系统通知</p>
</blockquote>
</li>
<li><p>虚引用必须和引用队列一起使用。虚引用在创建时必须提供一个引用队列作为参数。当垃圾回收器准备回收一个对象时，如果发现它还有虚引用，就会在回收对象后，将这个虚引用加入引用队列，以通知应用程序对象的回收情况</p>
</li>
<li><p><strong>由于虚引用可以跟踪对象的回收时间，因此也可以将一些资源释放操作放置在虚引用中执行和记录</strong></p>
</li>
<li><p>在 JDK1.2 版之后提供了 <code>PhantomReference</code> 类来实现虚引用</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="type">Object</span> <span class="variable">obj</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Object</span>(); <span class="comment">// 声明强引用</span></span><br><span class="line"><span class="type">ReferenceQueue</span> <span class="variable">phantomQueue</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ReferenceQueue</span>();</span><br><span class="line">PhantomReference&lt;Object&gt; sf = <span class="keyword">new</span> <span class="title class_">PhantomReference</span>&lt;&gt;(obj, phantomQueue);</span><br><span class="line">obj = <span class="literal">null</span>;</span><br></pre></td></tr></table></figure></li>
</ul>
<h4 id="11-终结器引用"><a href="#11-终结器引用" class="headerlink" title="11.终结器引用"></a>11.终结器引用</h4><ul>
<li>它<strong>用于实现对象的 finalize() 方法</strong>，也可以称为终结器引用。<strong>无需手动编码，其内部配合引用队列使用</strong></li>
<li>在 GC 时，终结器引用入队。由 Finalizer 线程通过终结器引用找到被引用对象调用它的 <code>finalize()</code> 方法，第二次 GC 时才回收被引用的对象</li>
</ul>
<h3 id="4-垃圾回收器"><a href="#4-垃圾回收器" class="headerlink" title="4.垃圾回收器"></a>4.垃圾回收器</h3><h4 id="1-GC-分类与性能指标"><a href="#1-GC-分类与性能指标" class="headerlink" title="1.GC 分类与性能指标"></a>1.GC 分类与性能指标</h4><ol>
<li><p>垃圾回收器概述</p>
<blockquote>
<p>垃圾收集器没有在规范中进行过多的规定，可以由不同的厂商、不同版本的 JVM 来实现</p>
<p>由于 JDK 的版本处于高速迭代过程中，因此 Java 发展至今已经衍生了众多的 GC 版本</p>
<p>从不同角度分析垃圾收集器，可以将 GC 分为不同的类型</p>
</blockquote>
</li>
<li><p>垃圾回收器分类</p>
<ul>
<li><p><strong>按线程数分：串行垃圾回收器 VS 并行垃圾回收器</strong></p>
<ul>
<li><p>串行回收：在同一时间段内<strong>只允许有一个 CPU 用于执行垃圾回收操作</strong>，此时工作线程被暂停，直至垃圾收集工作结束</p>
<blockquote>
<p>在诸如单 CPU 处理器或者较小的应用内存等硬件平台不是特别优越的场合，串行回收器的性能表现可以超过并行回收器和并发回收器</p>
<p><strong>串行回收默认被应用在客户端的 Client 模式下的 JVM 中</strong></p>
<p>在并发能力比较强的 CPU 上，并行回收器产生的停顿时间要短于串行回收器</p>
</blockquote>
</li>
<li><p>并行回收：运用多个 CPU 同时执行垃圾回收，因此提升了应用的吞吐量，不过并行回收仍然与串行回收一样，采用独占式，使用了 <code>Stop-the-World</code> 机制</p>
</li>
</ul>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/%E6%8C%89%E7%BA%BF%E7%A8%8B%E6%95%B0%E5%88%92%E5%88%86.png" alt="按线程数划分"></p>
</li>
<li><p><strong>按工作模式分：并发式垃圾回收器 VS 独占式垃圾回收器</strong></p>
<ul>
<li>并发式垃圾回收器：垃圾回收线程与应用程序线程交替工作，以尽可能减少应用程序的停顿时间</li>
<li>独占式垃圾回收器（Stop the world）：一旦运行，就停止应用程序中的所有用户线程，直到垃圾回收过程完全结束</li>
</ul>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/%E6%8C%89%E5%B7%A5%E4%BD%9C%E6%A8%A1%E5%BC%8F%E5%88%92%E5%88%86.png" alt="按工作模式划分"></p>
</li>
<li><p><strong>按碎片处理方式分：压缩式垃圾回收器 VS 非压缩式垃圾回收器</strong></p>
<ul>
<li>压缩式垃圾回收器：在回收完成后，对存活对象进行压缩整理，消除回收后的碎片</li>
<li>非压缩式的垃圾回收器：不进行这步操作</li>
</ul>
</li>
<li><p><strong>按工作的内存区间分：年轻代垃圾回收器 VS 老年代垃圾回收器</strong></p>
</li>
</ul>
</li>
<li><p>评估 GC 的性能指标</p>
<ul>
<li><p><strong>吞吐量</strong>：运行用户代码的时间占总运行时间的比例（总运行时间 &#x3D; 程序的运行时间 + 内存回收的时间）</p>
<blockquote>
<p>吞吐量就是 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值，即吞吐量 &#x3D; 运行用户代码时间 &#x2F;（运行用户代码时间 + 垃圾收集时间）。比如：虚拟机总共运行了 100 分钟，其中垃圾收集花掉 1 分钟，那吞吐量就是 99%</p>
<p>这种情况下，应用程序能容忍较高的暂停时间，因此高吞吐量的应用程序有更长的时间基准，快速响应是不必考虑的</p>
<p>吞吐量优先，意味着在单位时间内，STW 的时间最短：0.2 + 0.2 &#x3D; 0.4</p>
</blockquote>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/%E6%B3%A8%E9%87%8D%E5%90%9E%E5%90%90%E9%87%8F.png" alt="注重吞吐量"></p>
</li>
<li><p>垃圾收集开销：吞吐量的补数，垃圾收集所用时间与总运行时间的比例</p>
</li>
<li><p><strong>暂停时间</strong>：执行垃圾收集时，程序的工作线程被暂停的时间</p>
<blockquote>
<p><code>暂停时间</code>是指一个时间段内应用程序线程暂停，让 GC 线程执行的状态</p>
<p>例如，GC 期间 100 毫秒的暂停时间意味着在这 100 毫秒期间内没有应用程序线程是活动的</p>
<p>暂停时间优先，意味着尽可能让单次 STW 的时间最短：0.1 + 0.1 + 0.1 + 0.1 + 0.1 &#x3D; 0.5</p>
</blockquote>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/%E6%B3%A8%E9%87%8D%E6%9A%82%E5%81%9C%E6%97%B6%E9%97%B4.png" alt="注重暂停时间"></p>
</li>
<li><p>收集频率：相对于应用程序的执行，收集操作发生的频率</p>
</li>
<li><p><strong>内存占用</strong>：Java 堆区所占的内存大小</p>
</li>
<li><p>快速：一个对象从诞生到被回收所经历的时间</p>
</li>
</ul>
<blockquote>
<p>吞吐量、暂停时间、内存占用 这三者共同构成一个<code>不可能三角</code></p>
<p>三者总体的表现会随着技术进步而越来越好。一款优秀的收集器通常最多同时满足其中的两项</p>
<p>这三项里，暂停时间的重要性日益凸显。因为随着硬件发展，内存占用多些越来越能容忍，硬件性能的提升也有助于降低收集器运行时对应用程序的影响，即提高了吞吐量。而内存的扩大，对延迟反而带来负面效果</p>
<p><strong>简单来说，主要抓住两点：吞吐量、暂停时间</strong></p>
</blockquote>
<ul>
<li><p>吞吐量 vs 暂停时间</p>
<blockquote>
<p>高吞吐量较好因为这会让应用程序的最终用户感觉只有应用程序线程在做生产性工作</p>
<p>直觉上，吞吐量越高程序运行越快</p>
<p>低暂停时间（低延迟）较好因为从最终用户的角度来看不管是 GC 还是其他原因导致一个应用被挂起始终是不好的。这取决于应用程序的类型，有时候甚至短暂的 200 毫秒暂停都可能打断终端用户体验</p>
<p>因此具有低的较大暂停时间是非常重要的，特别是对于一个交互式应用程序</p>
<p>不幸的是<code>高吞吐量</code>和<code>低暂停时间</code>是一对相互竞争的目标（矛盾）</p>
<ul>
<li>因为如果选择以吞吐量优先，那么必然需要降低内存回收的执行频率，但是这样会导致 GC 需要更长的暂停时间来执行内存回收</li>
<li>相反的，如果选择以低延迟优先为原则，那么为了降低每次执行内存回收时的暂停时间，也只能频繁地执行内存回收，但这又引起了年轻代内存的缩减和导致程序吞吐量的下降</li>
</ul>
<p>在设计（或使用）GC 算法时，我们必须确定我们的目标：</p>
<p>一个 GC 算法只可能针对两个目标之一（即只专注于较大吞吐量或最小暂停时间），或尝试找到一个二者的折衷</p>
<p><strong>现在标准：在最大吞吐量优先的情况下，降低停顿时间</strong></p>
</blockquote>
</li>
</ul>
</li>
</ol>
<h4 id="2-不同的垃圾回收器概述"><a href="#2-不同的垃圾回收器概述" class="headerlink" title="2.不同的垃圾回收器概述"></a>2.不同的垃圾回收器概述</h4><blockquote>
<p>垃圾收集机制是 Java 的招牌能力，极大地提高了开发效率。这当然也是面试的热点</p>
<p>为什么要有很多收集器，一个不够吗？</p>
<p>因为 Java 的使用场景很多，移动端，服务器等。所以就需要针对不同的场景，提供不同的垃圾收集器，提高垃圾收集的性能</p>
<p>虽然我们会对各个收集器进行比较，但并非为了挑选一个最好的收集器出来</p>
<p>没有一种放之四海皆准、任何场景下都适用的完美收集器存在，更加没有万能的收集器。所以<strong>选择的只是对具体应用最合适的收集器</strong></p>
</blockquote>
<ol>
<li><p>垃圾回收器发展史</p>
<blockquote>
<p>有了虚拟机，就一定需要收集垃圾的机制，这就是 Garbage Collection，对应的产品我们称为 Garbage Collector</p>
</blockquote>
<ul>
<li><p>1999 年随 JDK1.3.1 一起来的是串行方式的 <code>serialGC</code>，它是第一款 GC。<code>ParNew</code> 垃圾收集器是 Serial 收集器的多线程版本</p>
</li>
<li><p>2002 年 2 月 26 日，<code>Parallel GC</code> 和 <code>Concurrent Mark Sweep GC</code> 跟随 JDK1.4.2 一起发布·</p>
</li>
<li><p><code>Parallel GC</code> 在 JDK6 之后成为 <code>HotSpot</code> 默认 GC</p>
</li>
<li><p>2012 年，在 JDK1.7u4 版本中，G1 可用</p>
</li>
<li><p>2017 年，JDK9 中 G1 变成默认的垃圾收集器，以替代 CMS</p>
<p>2018 年 3 月，JDK10 中 G1 垃圾回收器的并行完整垃圾回收，实现并行性来改善最坏情况下的延迟。</p>
</li>
<li><p>2018 年 9 月，JDK11 发布。引入 Epsilon 垃圾回收器，又被称为 <code>No-Op(无操作)</code> 回收器。同时，引入 ZGC：可伸缩的低延迟垃圾回收器（Experimental）</p>
</li>
<li><p>2019 年 3 月，JDK12 发布。增强 G1，自动返回未用堆内存给操作系统。同时，引入 Shenandoah GC：低停顿时间的 GC（Experimental）</p>
</li>
<li><p>2019 年 9 月，JDK13 发布。增强 ZGC，自动返回未用堆内存给操作系统</p>
</li>
<li><p>2020 年 3 月，JDK14 发布。删除 CMS 垃圾回收器。扩展 ZGC 在 <code>MACos</code> 和 <code>Windows</code> 上的应用</p>
</li>
</ul>
</li>
<li><p>7 种经典的垃圾回收器</p>
<blockquote>
<p><a target="_blank" rel="noopener" href="https://www.oracle.com/technetwork/java/javase/tech/memorymanagement-whitepaper-1-150020.pdf">官方网址</a></p>
</blockquote>
<ul>
<li>串行回收器：<code>Serial、Serial Old</code></li>
<li>并行回收器：<code>ParNew、Parallel Scavenge、Parallel old</code></li>
<li>并发回收器：<code>CMS、G1</code></li>
</ul>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/%E7%BB%8F%E5%85%B8%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E5%99%A8.png" alt="经典垃圾回收器"></p>
</li>
<li><p>垃圾回收器与垃圾分代之间的关系</p>
<ul>
<li><p>新生代收集器：<code>Serial、ParNew、Parallel Scavenge</code></p>
</li>
<li><p>老年代收集器：<code>Serial Old、Parallel Old、CMS</code></p>
</li>
<li><p>整堆收集器：<code>G1</code></p>
</li>
</ul>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E5%99%A8%E4%B8%8E%E5%88%86%E4%BB%A3%E5%85%B3%E7%B3%BB.png" alt="垃圾收集器与分代关系"></p>
</li>
<li><p>垃圾回收器的组合关系</p>
<ul>
<li>两个收集器间有连线，表明它们可以搭配使用：<code>Serial+Serial Old、Serial+CMS、ParNew+Serial Old、ParNew+CMS、Parallel Scavenge+Serial Old、Parallel Scavenge+Parallel Old、G1</code></li>
<li>其中 Serial Old 作为 CMS 出现 <code>Concurrent Mode Failure</code> 失败的后备预案</li>
<li>（红色虚线）由于维护和兼容性测试的成本，在 JDK 8 时将 <code>Serial+CMS、ParNew+Serial Old</code> 这两个组合声明为废弃（JEP173），并在 JDK9 中完全取消了这些组合的支持（JEP214）</li>
<li>（绿色虚线）JDK14 中：弃用 <code>Parallel Scavenge+Serialold GC</code> 组合（JEP366）</li>
<li>（绿色虚框）JDK14 中：删除 CMS 垃圾回收器（JEP363）</li>
</ul>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E5%99%A8%E7%BB%84%E5%90%88%E5%85%B3%E7%B3%BB.png" alt="垃圾收集器组合关系"></p>
</li>
<li><p>查看默认的垃圾回收器</p>
<blockquote>
<p><code>-XX:+PrintCommandLineFlags</code>：查看命令行相关参数（包含使用的垃圾收集器）</p>
<p>使用命令行指令：<code>jinfo -flag 相关垃圾回收器参数 进程ID</code></p>
</blockquote>
</li>
</ol>
<h4 id="3-Serial-x2F-Serial-Old-回收器：串行回收"><a href="#3-Serial-x2F-Serial-Old-回收器：串行回收" class="headerlink" title="3.Serial &#x2F; Serial Old 回收器：串行回收"></a>3.Serial &#x2F; Serial Old 回收器：串行回收</h4><ul>
<li><p>Serial</p>
<blockquote>
<p>Serial 收集器是最基本、历史最悠久的垃圾收集器了。JDK1.3 之前回收新生代唯一的选择</p>
<p>Serial 收集器作为 HotSpot 中 client 模式下的默认新生代垃圾收集器</p>
<p>Serial 收集器采用<strong>复制算法</strong>、串行回收和 <code>Stop-The-World</code> 机制的方式执行内存回收</p>
</blockquote>
</li>
<li><p>Serial Old</p>
<blockquote>
<p>除了年轻代之外，Serial 收集器还提供用于执行老年代垃圾收集的 <code>Serial Old</code> 收集器</p>
<p>Serial Old 收集器同样也采用了串行回收和 <code>Stop the World</code> 机制，只不过内存回收算法使用的是<strong>标记-压缩算法</strong></p>
<ul>
<li>Serial old 是运行在 Client 模式下默认的老年代的垃圾回收器</li>
<li>Serial 0ld 在 Server 模式下主要有两个用途：<ul>
<li>与新生代的 Parallel scavenge 配合使用</li>
<li>作为老年代 CMS 收集器的后备垃圾收集方案</li>
</ul>
</li>
</ul>
</blockquote>
</li>
</ul>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/Serial%E6%94%B6%E9%9B%86%E5%99%A8.png" alt="Serial收集器"></p>
<blockquote>
<p>这个收集器是一个单线程的收集器，但它的<code>单线程</code>的意义<strong>并不仅仅说明它只会使用一个 CPU 或一条收集线程去完成垃圾收集工作</strong>，更重要的是在它<strong>进行垃圾收集时，必须暂停其他所有的工作线程，直到它收集结束（Stop The World）</strong></p>
<p>优势：简单而高效（与其他收集器的单线程比），对于限定单个 CPU 的环境来说，Serial 收集器由于没有线程交互的开销，专心做垃圾收集自然可以获得最高的单线程收集效率。运行在 Client 模式下的虚拟机是个不错的选择</p>
<p>在用户的桌面应用场景中，可用内存一般不大（几十 MB 至一两百 MB），可以在较短时间内完成垃圾收集（几十 ms 至一百多 ms），只要不频繁发生，使用串行回收器是可以接受的</p>
<p>在 HotSpot 虚拟机中，使用<code>-XX:+UseSerialGC</code>参数可以指定年轻代和老年代都使用串行收集器。等价于新生代用 Serial GC，且老年代用 Serial Old GC</p>
</blockquote>
<ul>
<li><p><strong>总结</strong></p>
<blockquote>
<p>这种垃圾收集器大家了解，现在已经不用串行的了。而且在限定单核 cpu 才可以用。现在都不是单核的了</p>
<p>对于交互较强的应用而言，这种垃圾收集器是不能接受的。一般在 Java web 应用程序中是不会采用串行垃圾收集器的</p>
</blockquote>
</li>
</ul>
<h4 id="4-ParNew-回收器：并行回收"><a href="#4-ParNew-回收器：并行回收" class="headerlink" title="4.ParNew 回收器：并行回收"></a>4.ParNew 回收器：并行回收</h4><ul>
<li>如果说 Serial GC 是年轻代中的单线程垃圾收集器，<strong>那么 ParNew 收集器则是 Serial 收集器的多线程版本</strong></li>
<li><strong>Par 是 Parallel 的缩写，New：只能处理的是新生代</strong></li>
<li>ParNew 收集器除了采用<strong>并行回收</strong>的方式执行内存回收外，两款垃圾收集器之间几乎没有任何区别ParNew 收集器在年轻代中<strong>同样也是采用复制算法、<code>Stop-the-World</code> 机制</strong></li>
<li><strong>ParNew 是很多 JVM 运行在 Server 模式下新生代的默认垃圾收集器</strong></li>
</ul>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/ParNew%E6%94%B6%E9%9B%86%E5%99%A8.png" alt="ParNew收集器"></p>
<blockquote>
<p>对于新生代，回收次数频繁，使用并行方式高效</p>
<p>对于老年代，回收次数少，使用串行方式节省资源（CPU 并行需要切换线程，串行可以省去切换线程的资源）</p>
</blockquote>
<blockquote>
<p>由于 ParNew 收集器是基于并行回收，那么是否可以断定 ParNew 收集器的回收效率在任何场景下都会比 serial 收集器更高效？</p>
<ul>
<li>ParNew 收集器运行在多 CPU 的环境下，由于可以充分利用多 CPU、多核心等物理硬件资源优势，可以更快速地完成垃圾收集，提升程序的吞吐量</li>
<li>但是<strong>在单个 CPU 的环境下，ParNew 收集器不比 Serial 收集器更高效</strong>。虽然 Serial 收集器是基于串行回收，但是由于 CPU 不需要频繁地做任务切换，因此可以有效避免多线程交互过程中产生的一些额外开销</li>
</ul>
</blockquote>
<blockquote>
<p>因为除 Serial 外，目前只有 ParNew GC 能与 CMS 收集器配合工作</p>
<p>在程序中，开发人员可以通过选项 <code>-XX:+UseParNewGC</code> 手动指定使用 ParNew 收集器执行内存回收任务。它表示年轻代使用并行收集器，不影响老年代</p>
<p><code>-XX:ParallelGCThreads</code>限制线程数量，<strong>默认开启和 CPU 数据相同的线程数</strong></p>
</blockquote>
<h4 id="5-Parallel-Scavenge-x2F-Parallel-Old-回收器：吞吐量优先"><a href="#5-Parallel-Scavenge-x2F-Parallel-Old-回收器：吞吐量优先" class="headerlink" title="5.Parallel Scavenge &#x2F; Parallel Old 回收器：吞吐量优先"></a>5.Parallel Scavenge &#x2F; Parallel Old 回收器：吞吐量优先</h4><ul>
<li>Parallel Scavenge<ul>
<li>HotSpot 的年轻代中除了拥有 ParNew 收集器是基于并行回收的以外，Parallel Scavenge 收集器同样也采用了<strong>复制算法、并行回收和 <code>Stop the World</code> 机制</strong></li>
<li>那么 Parallel 收集器的出现是否多此一举？<ul>
<li>和 ParNew 收集器不同，ParallelScavenge 收集器的目标则是达到一个<strong>可控制的吞吐量</strong>（Throughput），它也被称为<strong>吞吐量优先的垃圾收集器</strong></li>
<li><strong>自适应调节策略也是 Parallel Scavenge 与 ParNew 一个重要区别</strong></li>
</ul>
</li>
<li>高吞吐量则可以高效率地利用 CPU 时间，尽快完成程序的运算任务，<strong>主要适合在后台运算而不需要太多交互的任务</strong>。因此常见在服务器环境中使用。<strong>例如：执行批量处理、订单处理、工资支付、科学计算的应用程序</strong></li>
</ul>
</li>
<li>Parallel Old<ul>
<li>Parallel 收集器在 JDK1.6 时提供了用于执行老年代垃圾收集的 Parallel Old 收集器，用来代替老年代的 Serial Old 收集器</li>
<li>Parallel Old 收集器采用了<strong>标记-压缩算法</strong>，但同样也是基于<strong>并行回收和 <code>Stop-the-World</code> 机制</strong></li>
</ul>
</li>
</ul>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/Parallel%E6%94%B6%E9%9B%86%E5%99%A8.png" alt="Parallel收集器"></p>
<ul>
<li><p>在程序吞吐量优先的应用场景中，Parallel 收集器和 Parallel Old 收集器的组合，在 Server 模式下的内存回收性能很不错。<strong>在 Java8 中，默认是此垃圾收集器</strong></p>
</li>
<li><p><strong>参数配置</strong></p>
<ul>
<li><p><code>-XX:+UseParallelGC</code> 手动指定年轻代使用 Parallel 并行收集器执行内存回收任务</p>
</li>
<li><p><code>-XX:+UseParallelOldGC</code> 手动指定老年代都是使用并行回收收集器</p>
<blockquote>
<p>分别适用于新生代和老年代。默认 jdk8 是开启的</p>
<p>上面两个参数，默认开启一个，另一个也会被开启（互相激活）</p>
</blockquote>
</li>
<li><p><code>-XX:ParallelGCThreads</code> 设置年轻代并行收集器的线程数。一般与 CPU 数量相等，以避免过多的线程数影响垃圾收集性能</p>
<p>$$ ParallelGCThreads &#x3D; \begin{cases} CPU_Count &amp; \text (CPU_Count &lt;&#x3D; 8) \ 3 + (5 * CPU＿Count &#x2F; 8) &amp; \text (CPU_Count &gt; 8) \end{cases} $$</p>
</li>
<li><p><code>-XX:MaxGCPauseMillis</code> 设置垃圾收集器最大停顿时间（即 STW 的时间）单位是毫秒</p>
<ul>
<li>为了尽可能地把停顿时间控制在 MaxGCPauseMills 以内，收集器在工作时会调整 Java 堆大小或者其他一些参数</li>
<li>对于用户来讲，停顿时间越短体验越好。但是在服务器端，我们注重高并发，整体的吞吐量。所以服务器端适合 Parallel 进行控制</li>
<li><strong>该参数使用需谨慎</strong></li>
</ul>
</li>
<li><p><code>-XX:GCTimeRatio</code> 垃圾收集时间占总时间的比例（&#x3D;1&#x2F;（N+1））。用于衡量吞吐量的大小</p>
<ul>
<li><strong>取值范围（0, 100）。默认值 99，也就是垃圾回收时间不超过 1%</strong></li>
<li>与前一个<code>-XX:MaxGCPauseMillis </code>参数有一定矛盾性。暂停时间越长，Radio 参数就容易超过设定的比例</li>
</ul>
</li>
<li><p><code>-XX:+UseAdaptivesizePolicy</code> <strong>设置 Parallel Scavenge 收集器具有自适应调节策略</strong></p>
<ul>
<li>在这种模式下，年轻代的大小、Eden 和 Survivor 的比例、晋升老年代的对象年龄等参数会被自动调整，已达到在堆大小、吞吐量和停顿时间之间的平衡点</li>
<li>在手动调优比较困难的场合，可以直接使用这种自适应的方式，仅指定虚拟机的最大堆、目标的吞吐量（<code>GCTimeRatio</code>）和停顿时间（<code>MaxGCPauseMills</code>），让虚拟机自己完成调优工作</li>
</ul>
</li>
</ul>
</li>
</ul>
<h4 id="6-CMS-回收器：低延迟"><a href="#6-CMS-回收器：低延迟" class="headerlink" title="6.CMS 回收器：低延迟"></a>6.CMS 回收器：低延迟</h4><ul>
<li><p>CMS 概述</p>
<blockquote>
<p>在 JDK1.5 时期，HotSpot 推出了一款在<strong>强交互应用</strong>中几乎可认为有划时代意义的垃圾收集器：CMS<strong>（Concurrent-Mark-Sweep）</strong>收集器</p>
<p><strong>这款收集器是 HotSpot 虚拟机中第一款真正意义上的并发收集器，它第一次实现了让垃圾收集线程与用户线程同时工作</strong></p>
<p>CMS 收集器的关注点是<strong>尽可能缩短垃圾收集时用户线程的停顿时间</strong>。停顿时间越短（低延迟）就越适合与用户交互的程序，良好的响应速度能提升用户体验</p>
<p><strong>目前很大一部分的 Java 应用集中在互联网站或者 B&#x2F;S 系统的服务端上，这类应用尤其重视服务的响应速度，希望系统停顿时间最短</strong>，以给用户带来较好的体验。CMS 收集器就非常符合这类应用的需求</p>
<p>CMS 的垃圾收集算法采用<strong>标记-清除算法</strong>，并且也会 <code>Stop-the-World</code></p>
<p>不幸的是，CMS 作为老年代的收集器，却无法与 JDK1.4.0 中已经存在的新生代收集器 Parallel Scavenge 配合工作，所以在 JDK1.5 中使用 CMS 来收集老年代的时候，新生代只能选择 ParNew 或者 Serial 收集器中的一个</p>
<p>在 G1 出现之前，CMS 使用还是非常广泛的。一直到今天，仍然有很多系统使用 CMS GC</p>
</blockquote>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/CMS%E6%94%B6%E9%9B%86%E5%99%A8.png" alt="CMS收集器"></p>
</li>
<li><p>CMS 垃圾收集过程</p>
<blockquote>
<p>CMS 整个过程比之前的收集器要复杂，整个过程分为 4 个主要阶段</p>
<p>尽管 CMS 收集器采用的是并发回收（非独占式），但是<strong>在其初始化标记和再次标记这两个阶段中仍然需要执行 <code>Stop-the-World</code> 机制暂停程序中的工作线程</strong>，不过暂停时间并不会太长，因此可以说明目前所有的垃圾收集器都做不到完全不需要 <code>stop-the-World</code>，只是尽可能地缩短暂停时间</p>
<p><strong>由于最耗费时间的并发标记与并发清除阶段都不需要暂停工作，所以整体的回收是低停顿的</strong></p>
<p>另外，由于在垃圾收集阶段用户线程没有中断，<strong>所以在 CMS 回收过程中，还应该确保应用程序用户线程有足够的内存可用</strong></p>
<p>因此 CMS 收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集，而是<strong>当堆内存使用率达到某一阈值时，便开始进行回收，以确保应用程序在 CMS 工作过程中依然有足够的空间支持应用程序运行</strong></p>
<p>要是 CMS 运行期间预留的内存无法满足程序需要，就会出现一次 <code>Concurrent Mode Failure</code> 失败，<strong>这时虚拟机将启动后备预案：临时启用 Serial Old 收集器来重新进行老年代的垃圾收集</strong>，这样停顿时间就很长了</p>
<p>CMS 收集器的垃圾收集算法采用的是标记清除算法，这意味着每次执行完内存回收后，由于被执行内存回收的无用对象所占用的内存空间极有可能是不连续的一些内存块，<strong>不可避免地将会产生一些内存碎片</strong></p>
<p>那么 CMS 在为新对象分配内存空间时，将无法使用指针碰撞（Bump the Pointer）技术，而只能够选择空闲列表（Free List）执行内存分配</p>
</blockquote>
<ul>
<li><p>初始标记（Initial-Mark）阶段：</p>
<blockquote>
<p>在这个阶段中，程序中所有的工作线程都将会因为 <code>Stop-the-World</code> 机制而出现短暂的暂停，这个阶段的主要任务<strong>仅仅只是标记出 GCRoots 能直接关联到的对象</strong>。一旦标记完成之后就会恢复之前被暂停的所有应用线程。由于直接关联对象比较小，所以这里的速度非常快</p>
</blockquote>
</li>
<li><p>并发标记（Concurrent-Mark）阶段：</p>
<blockquote>
<p><strong>从 GC Roots 的直接关联对象开始遍历整个对象图的过程，这个过程耗时较长但是不需要停顿用户线程，可以与垃圾收集线程一起并发运行</strong></p>
</blockquote>
</li>
<li><p>重新标记（Remark）阶段：</p>
<blockquote>
<p>由于在并发标记阶段中，程序的工作线程会和垃圾收集线程同时运行或者交叉运行，<strong>因此为了修正并发标记期间，因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录，这个阶段的停顿时间通常会比初始标记阶段稍长一些</strong>，但也远比并发标记阶段的时间短</p>
</blockquote>
</li>
<li><p>并发清除（Concurrent-Sweep）阶段：</p>
<blockquote>
<p><strong>此阶段清理删除掉标记阶段判断的已经死亡的对象，释放内存空间</strong>。由于不需要移动存活对象，所以这个阶段也是可以与用户线程同时并发的</p>
</blockquote>
</li>
</ul>
<blockquote>
<p><strong>有人会觉得既然 Mark Sweep 会造成内存碎片，那么为什么不把算法换成 Mark Compact？</strong></p>
<p>因为当并发清除的时候，用 Compact 整理内存的话，原来的用户线程使用的内存还怎么用呢？</p>
<p>要保证用户线程能继续执行，前提的它运行的资源不受影响。Mark Compact 更适合 <code>Stop the World</code> 这种场景下使用</p>
</blockquote>
</li>
<li><p>CMS 总结</p>
<ul>
<li><p>优点</p>
<ul>
<li>并发收集</li>
<li>低延迟</li>
</ul>
</li>
<li><p>缺点</p>
<ul>
<li><p><strong>会产生内存碎片</strong></p>
<blockquote>
<p>导致并发清除后，用户线程可用的空间不足。在无法分配大对象的情况下，不得不提前触发 FullGC</p>
</blockquote>
</li>
<li><p><strong>CMS 收集器对 CPU 资源非常敏感</strong></p>
<blockquote>
<p>在并发阶段，它虽然不会导致用户停顿，但是会因为占用了一部分线程而导致应用程序变慢，总吞吐量会降低</p>
</blockquote>
</li>
<li><p>CMS 收集器无法处理浮动垃圾</p>
<blockquote>
<p>可能出现 <code>Concurrent Mode Failure</code> 失败而导致另一次 Full GC 的产生</p>
<p>在并发标记阶段由于程序的工作线程和垃圾收集线程是同时运行或者交叉运行的，那么在并发标记阶段如果产生新的垃圾对象，CMS 将无法对这些垃圾对象进行标记，最终会导致这些新产生的垃圾对象没有被及时回收，从而只能在下一次执行 GC 时释放这些之前未被回收的内存空间</p>
</blockquote>
</li>
</ul>
</li>
</ul>
</li>
<li><p>设置参数</p>
<ul>
<li><p><code>-XX:+UseConcMarkSweepGC </code>手动指定使用 CMS 收集器执行内存回收任务</p>
<blockquote>
<p>开启该参数后会自动将 <code>-xx:+UseParNewGC</code> 打开</p>
<p>即：ParNew（Young 区用）+CMS（Old 区用）+ Serial Old 的组合</p>
</blockquote>
</li>
<li><p><code>-XX:CMSInitiatingOccupanyFraction</code> 设置堆内存使用率的阈值，一旦达到该阈值，便开始进行回收</p>
<blockquote>
<p>JDK5 及以前版本的默认值为 68，即当老年代的空间使用率达到 68%时，会执行一次 CMS 回收</p>
<p><strong>JDK6 及以上版本默认值为 92%</strong></p>
<p>如果内存增长缓慢，则可以设置一个稍大的值，大的阀值可以有效降低 CMS 的触发频率，减少老年代回收的次数可以较为明显地改善应用程序性能</p>
<p>反之，如果应用程序内存使用率增长很快，则应该降低这个阈值，以避免频繁触发老年代串行收集器。因此通过该选项便可以有效降低 FullGC 的执行次数</p>
</blockquote>
</li>
<li><p><code>-XX:+UseCMSCompactAtFullCollection</code> 用于指定在执行完 Full GC 后对内存空间进行压缩整理，以此避免内存碎片的产生</p>
<blockquote>
<p>不过由于内存压缩整理过程无法并发执行，所带来的问题就是停顿时间变得更长了</p>
</blockquote>
</li>
<li><p><code>-XX:CMSFullGCsBeforeCompaction</code> 设置在执行多少次 Full GC 后对内存空间进行压缩整理</p>
</li>
<li><p><code>-XX:ParallelcMSThreads</code> 设置 CMS 的线程数量</p>
<blockquote>
<p>CMS 默认启动的线程数是（ParallelGCThreads+3）&#x2F;4，ParallelGCThreads 是年轻代并行收集器的线程数。当 CPU 资源比较紧张时，受到 CMS 收集器线程的影响，应用程序的性能在垃圾回收阶段可能会非常糟糕</p>
</blockquote>
</li>
</ul>
</li>
<li><p>JDK 后续版本中 CMS 的变化</p>
<ul>
<li><p>JDK9 新特性：CMS 被标记为 Deprecate 了（JEP291）</p>
<blockquote>
<p>如果对 JDK9 及以上版本的 HotSpot 虚拟机使用参数<code>-XX: +UseConcMarkSweepGC</code>来开启 CMS 收集器的话，用户会收到一个警告信息，提示 CMS 未来将会被废弃</p>
</blockquote>
</li>
<li><p>JDK14 新特性：删除 CMS 垃圾回收器（JEP363）</p>
<blockquote>
<p>移除了 CMS 垃圾收集器，如果在 JDK14 中使用 <code>-XX:+UseConcMarkSweepGC</code>的话，JVM 不会报错，只是给出一个 warning 信息，但是不会 exit</p>
<p>JVM 会自动回退以默认 GC 方式启动 JVM</p>
</blockquote>
</li>
</ul>
</li>
</ul>
<h4 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h4><blockquote>
<p>HotSpot 有这么多的垃圾回收器，那么如果有人问，Serial GC、Parallel GC、Concurrent Mark Sweep GC 这三个 Gc 有什么不同呢？请记住以下口令：</p>
<ul>
<li>如果你想要最小化地使用内存和并行开销，请选 Serial GC</li>
<li>如果你想要最大化应用程序的吞吐量，请选 Parallel GC</li>
<li>如果你想要最小化 GC 的中断或停顿时间，请选 CMS GC</li>
</ul>
</blockquote>
<h4 id="7-G1-回收器：区域化分代式"><a href="#7-G1-回收器：区域化分代式" class="headerlink" title="7.G1 回收器：区域化分代式"></a>7.G1 回收器：区域化分代式</h4><ul>
<li><p>G1 回收器概述</p>
<blockquote>
<p><strong>既然我们已经有了前面几个强大的 GC，为什么还要发布 Garbage First（G1）？</strong></p>
<p>原因就在于应用程序<strong>所应对的业务越来越庞大、复杂，用户越来越多</strong>，没有 GC 就不能保证应用程序正常进行，而经常造成 STW 的 GC 又跟不上实际的需求，所以才会不断地尝试对 GC 进行优化</p>
<p>G1（Garbage-First）垃圾回收器是在 Java7 update4 之后引入的一个新的垃圾回收器，是当今收集器技术发展的最前沿成果之一</p>
<p>与此同时，为了适应现在不断扩大的内存和不断增加的处理器数量，进一步降低暂停时间（pause time），同时兼顾良好的吞吐量</p>
<p>官方给 G1 设定的目标是在延迟可控的情况下获得尽可能高的吞吐量，所以才担当起<code>全功能收集器</code>的重任与期望</p>
</blockquote>
<blockquote>
<p><strong>为什么名字叫 Garbage First(G1)呢？</strong></p>
<p>因为 G1 是一个并行回收器，它把堆内存分割为很多不相关的区域（Region）（物理上不连续的）</p>
<p>使用不同的 Region 来表示 Eden、幸存者 0 区，幸存者 1 区，老年代等</p>
<p>G1 GC 有计划地避免在整个 Java 堆中进行全区域的垃圾收集</p>
<p>G1 跟踪各个 Region 里面的垃圾堆积的价值大小（回收所获得的空间大小以及回收所需时间的经验值），在后台维护一个优先列表，每次根据允许的收集时间，优先回收价值最大的 Region</p>
<p><strong>由于这种方式的侧重点在于回收垃圾最大量的区间（Region），所以我们给 G1 一个名字：垃圾优先（Garbage First）</strong></p>
<p>G1（Garbage-First）是一款<strong>面向服务端应用的垃圾收集器，主要针对配备多核 CPU 及大容量内存的机器，以极高概率满足 GC 停顿时间的同时，还兼具高吞吐量的性能特征</strong></p>
<p>在 JDK1.7 版本正式启用，移除了 Experimenta1 的标识，<strong>是JDK9 以后的默认垃圾回收器</strong>，取代了 CMS 回收器以及 Parallel+Parallel Old 组合。被 Oracle 官方称为<code>全功能的垃圾收集器</code></p>
<p>与此同时，CMS 已经在 JDK9 中被标记为废弃（deprecated）</p>
<p>在 jdk8 中还不是默认的垃圾回收器，需要使用<code>-XX:+UseG1GC</code>来启用</p>
</blockquote>
</li>
<li><p>G1 回收器特点</p>
<blockquote>
<p>与其他 GC 收集器相比，G1 使用了全新的分区算法，其特点如下所示：</p>
</blockquote>
<ul>
<li><p><strong>并行与并发</strong></p>
<ul>
<li><p>并行性</p>
<blockquote>
<p>G1 在回收期间，可以有多个 GC 线程同时工作，有效利用多核计算能力。此时用户线程 STW</p>
</blockquote>
</li>
<li><p>并发性</p>
<blockquote>
<p>G1 拥有与应用程序交替执行的能力，部分工作可以和应用程序同时执行，因此一般来说，不会在整个回收阶段发生完全阻塞应用程序的情况</p>
</blockquote>
</li>
</ul>
</li>
<li><p><strong>分代收集</strong></p>
<ul>
<li><strong>从分代上看，G1 依然属于分代型垃圾回收器，它会区分年轻代和老年代，年轻代依然有 Eden 区和 Survivor 区</strong>。但从堆的结构上看，它不要求整个 Eden 区、年轻代或者老年代都是连续的，也不再坚持固定大小和固定数量</li>
<li><strong>将堆空间分为若干个区域（Region）</strong>，这些区域中包含了逻辑上的年轻代和老年代</li>
<li><strong>和之前的各类回收器不同，它同时兼顾年轻代和老年代</strong>（其他回收器，只工作在年轻代或老年代)</li>
</ul>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/G101.png" alt="G101"></p>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/G102.png" alt="G102"></p>
</li>
<li><p><strong>空间整合</strong></p>
<ul>
<li><p>CMS：标记-清除算法、内存碎片、若干次 GC 后进行一次碎片整理</p>
</li>
<li><p>G1 将内存划分为一个个的 region，内存的回收是以 region 作为基本单位的</p>
<blockquote>
<p><strong>Region 之间是复制算法，但整体上实际可看作是标记-压缩（Mark-Compact）算法</strong>，两种算法都可以避免内存碎片</p>
<p>这种特性有利于程序长时间运行，分配大对象时不会因为无法找到连续内存空间而提前触发下一次 GC。尤其是当 Java 堆非常大的时候，G1 的优势更加明显</p>
</blockquote>
</li>
</ul>
</li>
<li><p><strong>可预测的停顿时间模型（即：软实时 soft real-time）</strong></p>
<ul>
<li><p>这是 G1 相对于 CMS 的另一大优势，G1 除了追求低停顿外，还能建立可预测的停顿时间模型，能让使用者明确指定在一个长度为 M 毫秒的时间片段内，消耗在垃圾收集上的时间不得超过 N 毫秒</p>
</li>
<li><p>由于分区的原因，G1 可以只选取部分区域进行内存回收，这样缩小了回收的范围，因此对于全局停顿情况的发生也能得到较好的控制</p>
</li>
<li><p>G1 跟踪各个 Region 里面的<strong>垃圾堆积的价值大小（回收所获得的空间大小以及回收所需时间的经验值）</strong>，在后台维护一个优先列表，<strong>每次根据允许的收集时间，优先回收价值最大的 Region</strong>。保证了 G1 收集器在有限的时间内可以获取尽可能高的收集效率</p>
</li>
<li><p><strong>相比于 CMS GC，G1 未必能做到 CMS 在最好情况下的延时停顿，但是最差情况要好很多</strong></p>
</li>
</ul>
</li>
</ul>
</li>
<li><p>G1 缺点</p>
<blockquote>
<p>相较于 CMS，G1 还不具备全方位、压倒性优势。比如在用户程序运行过程中，G1 无论是为了垃圾收集产生的内存占用（Footprint）还是程序运行时的额外执行负载（Overload）都要比 CMS 要高</p>
<p>从经验上来说，在小内存应用上 CMS 的表现大概率会优于 G1，而 G1 在大内存应用上则发挥其优势。平衡点在 6-8GB 之间</p>
</blockquote>
</li>
<li><p>G1 参数设置</p>
<ul>
<li><p><code>-XX:+UseG1GC</code>：手动指定使用 G1 垃圾收集器执行内存回收任务</p>
</li>
<li><p><code>-XX:G1HeapRegionSize</code> 设置每个 Region 的大小</p>
<blockquote>
<p>值是 2 的幂，范围是 1MB 到 32MB 之间，目标是根据最小的 Java 堆大小划分出约 2048 个区域。默认是堆内存的 1&#x2F;2000</p>
</blockquote>
</li>
<li><p><code>-XX:MaxGCPauseMillis</code> 设置期望达到的最大 GC 停顿时间指标（JVM 会尽力实现，但不保证达到）</p>
<blockquote>
<p>默认值是 200ms（人的平均反应速度）</p>
</blockquote>
</li>
<li><p><code>-XX:+ParallelGCThread</code> 设置 STW 工作线程数的值</p>
<blockquote>
<p>最多设置为 8（上面说过 Parallel 回收器的线程计算公式，当 CPU_Count &gt; 8 时，ParallelGCThreads 也会大于 8）</p>
</blockquote>
</li>
<li><p><code>-XX:ConcGCThreads</code> 设置并发标记的线程数</p>
<blockquote>
<p>将 n 设置为并行垃圾回收线程数（ParallelGCThreads）的 1&#x2F;4 左右</p>
</blockquote>
</li>
<li><p><code>-XX:InitiatingHeapOccupancyPercent</code> 设置触发并发 GC 周期的 Java 堆占用率阈值</p>
<blockquote>
<p>超过此值，就触发 GC</p>
<p>默认值是 45</p>
</blockquote>
</li>
</ul>
</li>
<li><p>G1 操作步骤</p>
<blockquote>
<p>G1 中提供了三种垃圾回收模式：Young GC、Mixed GC 和 Full GC，在不同的条件下被触发</p>
<p>G1 的设计原则就是简化 JVM 性能调优，开发人员只需要简单的三步即可完成调优：</p>
</blockquote>
<ul>
<li>第一步：开启 G1 垃圾收集器</li>
<li>第二步：设置堆的最大内存</li>
<li>第三步：设置最大的停顿时间</li>
</ul>
</li>
<li><p>G1 适用场景</p>
<blockquote>
<p>面向服务端应用，针对具有大内存、多处理器的机器（在普通大小的堆里表现并不惊喜）</p>
<p>最主要的应用是需要低 GC 延迟，并具有大堆的应用程序提供解决方案</p>
<p>如：在堆大小约 6GB 或更大时，可预测的暂停时间可以低于 0.5 秒；（G1 通过每次只清理一部分而不是全部的 Region 的增量式清理来保证每次 GC 停顿时间不会过长）</p>
<p><strong>用来替换掉 JDK1.5 中的 CMS 收集器；在下面的情况时，使用 G1 可能比 CMS 好：</strong></p>
<ul>
<li>超过 50%的 Java 堆被活动数据占用</li>
<li>对象分配频率或年代提升频率变化很大</li>
<li>GC 停顿时间过长（长于 0.5 至 1 秒）</li>
</ul>
<p>HotSpot 垃圾收集器里，除了 G1 以外，其他的垃圾收集器使用内置的 JVM 线程执行 GC 的多线程操作，而 G1 GC 可以采用应用线程承担后台运行的 GC 工作，即当 JVM 的 GC 线程处理速度慢时，系统会调用应用程序线程帮助加速垃圾回收过程</p>
</blockquote>
</li>
<li><p>分区 Region：化整为零</p>
<blockquote>
<p>使用 G1 收集器时，它将整个 Java 堆划分成约 2048 个大小相同的独立 Region 块，每个 Region 块大小根据堆空间的实际大小而定，整体被控制在 1MB 到 32MB 之间，且为 2 的 N 次幂，即 1MB，2MB，4MB，8MB，16MB，32MB。可以通过 <code>-XX:G1HeapRegionSize</code> 设定</p>
<p><strong>所有的 Region 大小相同，且在 JVM 生命周期内不会被改变</strong></p>
<p>虽然还保留有新生代和老年代的概念，但新生代和老年代不再是物理隔离的了，它们都是一部分 Region（不需要连续）的集合。<strong>通过 Region 的动态分配方式实现逻辑上的连续</strong></p>
</blockquote>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/G103.png" alt="G103"></p>
<blockquote>
<p><strong>一个 region 有可能属于 Eden，Survivor 或者 Old&#x2F;Tenured 内存区域；但是一个 region 只可能属于一个角色</strong></p>
<p>图中的 E 表示该 region 属于 Eden 内存区域，S 表示属于 survivor 内存区域，O 表示属于 Old 内存区域。图中空白的表示未使用的内存空间</p>
<p><strong>G1 垃圾收集器还增加了一种新的内存区域，叫做 Humongous 内存区域，如图中的 H 块。主要用于存储大对象，如果超过 1.5 个 region，就放到 H</strong>（<strong>humongous——巨大无比的</strong>）</p>
<p>设置 H 的原因：对于堆中的对象，默认直接会被分配到老年代，但是如果它是一个短期存在的大对象就会对垃圾收集器造成负面影响</p>
<p>为了解决这个问题，G1 划分了一个 Humongous 区，它用来专门存放大对象</p>
<p><strong>如果一个 H 区装不下一个大对象，那么 G1 会寻找连续的 H 区来存储</strong>。为了能找到连续的 H 区，有时候不得不启动 Full GC。G1 的大多数行为都把 H 区作为老年代的一部分来看待</p>
<p><strong>每个 Region 都是通过指针碰撞来分配空间</strong></p>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/region%E6%8C%87%E9%92%88%E7%A2%B0%E6%92%9E.png" alt="region指针碰撞"></p>
</blockquote>
</li>
<li><p>G1 回收过程概述</p>
<blockquote>
<p>G1GC 的垃圾回收过程主要包括如下三个环节：</p>
<ul>
<li><p>年轻代 GC（Young GC）</p>
</li>
<li><p>老年代并发标记过程（Concurrent Marking）</p>
</li>
<li><p>混合回收（Mixed GC）</p>
</li>
</ul>
<p>如果需要，单线程、独占式、高强度的 Full GC 还是继续存在的。它针对 GC 的评估失败提供了一种失败保护机制，即强力回收</p>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/G1%E5%9B%9E%E6%94%B6%E8%BF%87%E7%A8%8B.png" alt="G1回收过程"></p>
<p><strong>顺时针，Young GC -&gt; Young GC + Concurrent Mark-&gt; Mixed GC 顺序，进行垃圾回收</strong></p>
<p>应用程序分配内存，<strong>当年轻代的 Eden 区用尽时开始年轻代回收过程</strong>；G1 的年轻代收集阶段是一个<strong>并行的独占式收集器</strong>。在年轻代回收期，G1GC 暂停所有应用程序线程，启动多线程执行年轻代回收。然后<strong>从年轻代区间移动存活对象到 Survivor 区间或者老年区间</strong>，也有可能是两个区间都会涉及</p>
<p><strong>当堆内存使用达到一定值（默认 45%）时，开始老年代并发标记过程</strong></p>
<p><strong>标记完成马上开始混合回收过程</strong></p>
<p>对于一个混合回收期，G1 GC 从老年区间移动存活对象到空闲区间，这些空闲区间也就成为了老年代的一部分</p>
<p>老年代的 G1 回收器和其他 GC 不同，<strong>G1 的老年代回收器不需要整个老年代被回收，一次只需要扫描&#x2F;回收一小部分老年代的 Region 就可以了</strong>。同时，这个老年代 Region 是和年轻代一起被回收的</p>
<p>举个例子：</p>
<p>一个 Web 服务器，Java 进程最大堆内存为 4G，每分钟响应 1500 个请求，每 45 秒钟会新分配大约 2G 的内存。G1 会每 45 秒钟进行一次年轻代回收，每 31 个小时整个堆的使用率会达到 45%，会开始老年代并发标记过程，标记完成后开始四到五次的混合回收</p>
</blockquote>
</li>
<li><p>Remembered Set（记忆集）</p>
<blockquote>
<p>一个对象被不同区域引用的问题</p>
<p>一个 Region 不可能是孤立的，一个 Region 中的对象可能被其他任意 Region 中对象引用，判断对象存活时，是否需要扫描整个 Java 堆才能保证准确？</p>
<p>在其他的分代收集器，也存在这样的问题（而 G1 更突出）回收新生代也不得不同时扫描老年代？</p>
<p><strong>这样的话会降低 MinorGC 的效率</strong></p>
</blockquote>
<blockquote>
<p><strong>解决方法：</strong></p>
<p>无论 G1 还是其他分代收集器，<strong>JVM 都是使用 Remembered Set 来避免全局扫描</strong></p>
<p><strong>每个 Region 都有一个对应的 Remembered Set</strong></p>
<p>每次 Reference 类型数据写操作时，都会产生一个 Write Barrier（写屏障） 暂时中断操作</p>
<p>然后检查将要写入的引用指向的对象是否和该 Reference 类型数据在不同的 Region（其他收集器：检查老年代对象是否引用了新生代对象）</p>
<p>如果不同，通过 CardTable 把相关引用信息记录到引用指向对象的所在 Region 对应的 Remembered Set 中</p>
<p>当进行垃圾收集时，在 GC 根节点的枚举范围加入 Remembered Set；就可以保证不进行全局扫描，也不会有遗漏</p>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/RemeberSet.png" alt="RemeberSet"></p>
</blockquote>
</li>
<li><p>G1 回收过程一：年轻代 GC</p>
<blockquote>
<p>JVM 启动时，G1 先准备好 Eden 区，程序在运行过程中不断创建对象到 Eden 区，当 Eden 空间耗尽时，G1 会启动一次年轻代垃圾回收过程</p>
<p>年轻代垃圾回收只会回收 Eden 区和 Survivor 区</p>
<p>首先 G1 停止应用程序的执行（Stop-The-World），G1 创建回收集（Collection Set），回收集是指需要被回收的内存分段的集合，年轻代回收过程的回收集包含年轻代 Eden 区和 Survivor 区所有的内存分段</p>
</blockquote>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/G1%E5%B9%B4%E8%BD%BB%E4%BB%A3GC.png" alt="G1年轻代GC"></p>
<ul>
<li><p>第一阶段，扫描根</p>
<blockquote>
<p>根是指 static 变量指向的对象，正在执行的方法调用链条上的局部变量等</p>
<p>根引用连同 RSet 记录的外部引用作为扫描存活对象的入口</p>
</blockquote>
</li>
<li><p>第二阶段，更新 RSet</p>
<blockquote>
<p>处理 <code>dirty card queue</code> 中的 card，更新 RSet</p>
<p>此阶段完成后，<strong>RSet 可以准确的反映老年代对所在的内存分段中对象的引用</strong></p>
<p><code>dirty card queue</code>：对于应用程序中的引用赋值语句 object.field &#x3D; object，JVM 会在之前和之后执行特殊的操作以在 <code>dirty card queue</code> 中入队保存了对象引用信息的 card。在年轻代回收的时候，G1 会对 <code>dirty card queue</code> 中所有 card 进行处理，以更新 RSet，保证 RSet 实时准确的反映引用关系</p>
<p>引入 <code>dirty card queue</code> 是为了保证性能，RSet 的处理需要线程同步，开销会很大</p>
</blockquote>
</li>
<li><p>第三阶段，处理 RSet</p>
<blockquote>
<p>识别被老年代对象指向的 Eden 中的对象，这些被指向的 Eden 中的对象被认为是存活的对象</p>
</blockquote>
</li>
<li><p>第四阶段，复制对象</p>
<blockquote>
<p>此阶段，对象树被遍历，Eden 区内存段中存活的对象会被复制到 Survivor 区中空的内存分段</p>
<p>Survivor 区内存段中存活的对象如果年龄未达阈值，年龄会加 1，达到阀值会被会被复制到 Old 区中空的内存分段</p>
<p>如果 Survivor 空间不够，Eden 空间的部分数据会直接晋升到老年代空间</p>
</blockquote>
</li>
<li><p>第五阶段，处理引用</p>
<blockquote>
<p>处理 Soft，Weak，Phantom，Final，JNI Weak 等引用</p>
<p>最终 Eden 空间的数据为空，GC 停止工作，而目标内存中的对象都是连续存储的，没有碎片，所以复制过程可以达到内存整理的效果，减少碎片</p>
</blockquote>
</li>
</ul>
</li>
<li><p>G1 回收过程二：并发标记过程</p>
<ul>
<li><p>初始标记阶段</p>
<blockquote>
<p>标记从根节点直接可达的对象。这个阶段是 STW 的，并且会触发一次年轻代 GC</p>
</blockquote>
</li>
<li><p>根区域扫描（Root Region Scanning）</p>
<blockquote>
<p> G1 GC 扫描 Survivor 区直接可达的老年代区域对象，并标记被引用的对象</p>
<p>这一过程必须在 YoungGC 之前完成</p>
</blockquote>
</li>
<li><p>并发标记（Concurrent Marking）</p>
<blockquote>
<p>在整个堆中进行并发标记（和应用程序并发执行），此过程可能被 YoungGC 中断</p>
<p>在并发标记阶段，<strong>若发现区域对象中的所有对象都是垃圾，那这个区域会被立即回收</strong></p>
<p>同时并发标记过程中，会计算<strong>每个区域的对象活性（区域中存活对象的比例）</strong></p>
</blockquote>
</li>
<li><p>再次标记（Remark）</p>
<blockquote>
<p>由于应用程序持续进行，需要修正上一次的标记结果（是 STW 的）</p>
<p>G1 中采用了比 CMS 更快的初始快照算法：snapshot-at-the-beginning（SATB）</p>
</blockquote>
</li>
<li><p>独占清理（cleanup，STW）</p>
<blockquote>
<p>计算各个区域的存活对象和 GC 回收比例，并进行排序，识别可以混合回收的区域，为下阶段做铺垫（是 STW 的）</p>
<p>这个阶段并不会实际上去做垃圾的收集</p>
</blockquote>
</li>
<li><p>并发清理阶段</p>
<blockquote>
<p>识别并清理完全空闲的区域</p>
</blockquote>
</li>
</ul>
</li>
<li><p>G1 回收过程三：混合回收</p>
<blockquote>
<p>当越来越多的对象晋升到老年代 o1d region 时，为了避免堆内存被耗尽，虚拟机会触发一个混合的垃圾收集器，即 Mixed GC，该算法并不是一个 Old GC，除了回收整个 Young Region，还会回收一部分的 Old Region</p>
<p>这里需要注意：<strong>是一部分老年代，而不是全部老年代</strong></p>
<p>可以选择哪些 Old Region 进行收集，从而可以对垃圾回收的耗时时间进行控制。<strong>也要注意的是 Mixed GC 并不是 Full GC</strong></p>
</blockquote>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/G1%E6%B7%B7%E5%90%88%E5%9B%9E%E6%94%B6.png" alt="G1混合回收"></p>
<ul>
<li><p>并发标记结束以后，老年代中百分百为垃圾的内存分段被回收了，部分为垃圾的内存分段被计算了出来</p>
<blockquote>
<p>默认情况下，这些老年代的内存分段会分 8 次（可以通过<code>-XX:G1MixedGCCountTarget</code>设置）被回收</p>
</blockquote>
</li>
<li><p>混合回收的回收集（Collection Set）包括八分之一的老年代内存分段，Eden 区内存分段，Survivor 区内存分段</p>
<blockquote>
<p>混合回收的算法和年轻代回收的算法完全一样，只是回收集多了老年代的内存分段</p>
<p>具体过程请参考上面的年轻代回收过程。</p>
</blockquote>
</li>
<li><p>由于老年代中的内存分段默认分 8 次回收，G1 会优先回收垃圾多的内存分段</p>
<blockquote>
<p>垃圾占内存分段比例越高的，越会被先回收</p>
<p>并且有一个阈值会决定内存分段是否被回收，<code>-XX:G1MixedGCLiveThresholdPercent</code>，默认为 65%，意思是垃圾占内存分段比例要达到 65%才会被回收</p>
<p>如果垃圾占比太低，意味着存活的对象占比高，在复制的时候会花费更多的时间</p>
</blockquote>
</li>
<li><p>混合回收并不一定要进行 8 次</p>
<blockquote>
<p>有一个阈值<code>-XX:G1HeapWastePercent</code>，默认值为 10%，意思是允许整个堆内存中有 10%的空间被浪费，意味着如果发现可以回收的垃圾占堆内存的比例低于 10%，则不再进行混合回收。因为 GC 会花费很多的时间但是回收到的内存却很少</p>
</blockquote>
</li>
</ul>
</li>
<li><p>G1 回收过程四：Full GC</p>
<ul>
<li><p>G1 的初衷就是要避免 Full GC 的出现。但是如果上述方式不能正常工作，G1 会停止应用程序的执行（Stop-The-World），使用单线程的内存回收算法进行垃圾回收，性能会非常差，应用程序停顿时间会很长</p>
</li>
<li><p>要避免 Full GC 的发生，一旦发生需要进行调整</p>
<blockquote>
<p>什么时候会发生 Full GC 呢？</p>
<p>比如堆内存太小，当 G1 在复制存活对象的时候没有空的内存分段可用，则会回退到 Full GC，这种情况可以通过增大内存解决</p>
</blockquote>
</li>
<li><p>导致 G1 Full GC 的原因可能有两个</p>
<ul>
<li>Evacuation 的时候没有足够的 to-space 来存放晋升的对象</li>
<li>并发处理过程完成之前空间耗尽</li>
</ul>
</li>
</ul>
</li>
<li><p>G1 优化建议</p>
<blockquote>
<p>从 Oracle 官方透露出来的信息可获知，回收阶段（Evacuation）其实本也有想过设计成与用户程序一起并发执行，但这件事情做起来比较复杂，考虑到 G1 只是回一部分 Region，停顿时间是用户可控制的，所以并不迫切去实现，而选择把这个特性放到了 G1 之后出现的低延迟垃圾收集器（即 ZGC）中</p>
<p>另外，还考虑到 G1 不是仅仅面向低延迟，停顿用户线程能够最大幅度提高垃圾收集效率，为了保证吞吐量所以才选择了完全暂停用户线程的实现方案</p>
</blockquote>
<ul>
<li>年轻代太小<ul>
<li>避免使用<code>-Xmn</code>或<code>-XX:NewRatio</code>等相关选项显式设置年轻代大小</li>
<li>固定年轻代的大小会覆盖暂停时间目标</li>
</ul>
</li>
<li>暂停时间目标不要太过严苛<ul>
<li>G1 GC 的吞吐量目标是 90% 的应用程序时间和 10% 的垃圾回收时间</li>
<li>评估 G1 GC 的吞吐量时，暂停时间目标不要太严苛。目标太过严苛表示你愿意承受更多的垃圾回收开销，而这些会直接影响到吞吐量</li>
</ul>
</li>
</ul>
</li>
</ul>
<h4 id="8-垃圾回收器总结"><a href="#8-垃圾回收器总结" class="headerlink" title="8.垃圾回收器总结"></a>8.垃圾回收器总结</h4><ol>
<li><p>7 种经典垃圾回收器总结</p>
<blockquote>
<p>截止 JDK1.8，一共有 7 款不同的垃圾收集器。每一款的垃圾收集器都有不同的特点，在具体使用的时候，需要根据具体的情况选用不同的垃圾收集器</p>
<p>GC 发展阶段：Serial &#x3D;&gt; Parallel（并行）&#x3D;&gt; CMS（并发）&#x3D;&gt; G1 &#x3D;&gt; ZGC</p>
</blockquote>
<table>
<thead>
<tr>
<th align="center">垃圾收集器</th>
<th align="center">分类</th>
<th align="center">作用位置</th>
<th align="center">使用算法</th>
<th align="center">特点</th>
<th align="center">适用场景</th>
</tr>
</thead>
<tbody><tr>
<td align="center">Serial</td>
<td align="center">串行运行</td>
<td align="center">作用于新生代</td>
<td align="center">复制算法</td>
<td align="center">响应速度优先</td>
<td align="center">适用于单 CPU 环境下的 client 模式</td>
</tr>
<tr>
<td align="center">ParNew</td>
<td align="center">并行运行</td>
<td align="center">作用于新生代</td>
<td align="center">复制算法</td>
<td align="center">响应速度优先</td>
<td align="center">多 CPU 环境 Server 模式下与 CMS 配合使用</td>
</tr>
<tr>
<td align="center">Parallel</td>
<td align="center">并行运行</td>
<td align="center">作用于新生代</td>
<td align="center">复制算法</td>
<td align="center">吞吐量优先</td>
<td align="center">适用于后台运算而不需要太多交互的场景</td>
</tr>
<tr>
<td align="center">Serial Old</td>
<td align="center">串行运行</td>
<td align="center">作用于老年代</td>
<td align="center">标记-压缩算法</td>
<td align="center">响应速度优先</td>
<td align="center">适用于单 CPU 环境下的 Client 模式</td>
</tr>
<tr>
<td align="center">Parallel Old</td>
<td align="center">并行运行</td>
<td align="center">作用于老年代</td>
<td align="center">标记-压缩算法</td>
<td align="center">吞吐量优先</td>
<td align="center">适用于后台运算而不需要太多交互的场景</td>
</tr>
<tr>
<td align="center">CMS</td>
<td align="center">并发运行</td>
<td align="center">作用于老年代</td>
<td align="center">标记-清除算法</td>
<td align="center">响应速度优先</td>
<td align="center">适用于互联网或 B／S 业务</td>
</tr>
<tr>
<td align="center">G1</td>
<td align="center">并发、并行运行</td>
<td align="center">作用于新生代、老年代</td>
<td align="center">标记-压缩算法、复制算法</td>
<td align="center">响应速度优先</td>
<td align="center">面向服务端应用</td>
</tr>
</tbody></table>
</li>
<li><p>垃圾回收器组合</p>
<blockquote>
<p>不同厂商、不同版本的虚拟机实现差距比较大。HotSpot 虚拟机在 JDK7&#x2F;8 后所有收集器及组合如下图</p>
<ul>
<li><p>两个收集器间有连线，表明它们可以搭配使用：Serial&#x2F;Serial Old、Serial&#x2F;CMS、ParNew&#x2F;Serial Old、ParNew&#x2F;CMS、Parallel Scavenge&#x2F;Serial Old、Parallel Scavenge&#x2F;Parallel Old、G1</p>
</li>
<li><p>其中 Serial Old 作为 CMS 出现＂<code>Concurrent Mode Failure</code>＂失败的后备预案</p>
</li>
<li><p>（红色虚线）由于维护和兼容性测试的成本，在 JDK 8 时将 Serial ＋ CMS、ParNew ＋ Serial old 这两个组合声明为 Deprecated（JEP 173），并在 JDK 9 中完全取消了这些组合的支持（JEP214）</p>
</li>
<li><p>（绿色虚线）JDK 14 中：弃用 ParallelScavenge 和 SeriaOold GC 组合(JEP 366)</p>
</li>
<li><p>（绿色虚框）JDK 14 中：删除 CMS 垃圾回收器（JEP 363）</p>
</li>
</ul>
</blockquote>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E5%99%A8%E7%BB%84%E5%90%88.png" alt="垃圾收集器组合"></p>
</li>
<li><p>怎么选择合适的垃圾回收器</p>
<blockquote>
<p>Java 垃圾收集器的配置对于 JVM 优化来说是一个很重要的选择，选择合适的垃圾收集器可以让 JVM 的性能有一个很大的提升</p>
</blockquote>
<ul>
<li>选择垃圾收集器建议<ul>
<li><p>优先调整堆的大小让 JVM 自适应完成</p>
</li>
<li><p>如果内存小于 100M，使用串行收集器</p>
</li>
<li><p>如果是单核、单机程序，并且没有停顿时间的要求，串行收集器</p>
</li>
<li><p>如果是多 CPU、需要高吞吐量、允许停顿时间超过 1 秒，选择并行或者 JVM 自己选择</p>
</li>
<li><p>如果是多 CPU、追求低停顿时间，需快速响应（比如延迟不能超过 1 秒，如互联网应用），使用并发收集器</p>
</li>
<li><p><strong>官方推荐 G1，性能高。现在互联网的项目，基本都是使用 G1</strong></p>
</li>
</ul>
</li>
<li>最后说明<ul>
<li>没有最好的收集器，更没有万能的收集</li>
<li>调优永远是针对特定场景、特定需求，不存在一劳永逸的收集器</li>
</ul>
</li>
</ul>
</li>
<li><p>面试</p>
<ul>
<li><p>对于垃圾收集，面试官可以循序渐进从理论、实践各种角度深入，也未必是要求面试者什么都懂。但如果你懂得原理，一定会成为面试中的加分项。 这里较通用、基础性的部分如下：</p>
<ul>
<li><p>垃圾收集的算法有哪些？如何判断一个对象是否可以回收？</p>
</li>
<li><p>垃圾收集器工作的基本流程</p>
</li>
<li><p>另外需要多关注垃圾回收器这一章的各种常用的参数</p>
</li>
</ul>
</li>
</ul>
</li>
</ol>
<h4 id="9-GC-日志分析"><a href="#9-GC-日志分析" class="headerlink" title="9.GC 日志分析"></a>9.GC 日志分析</h4><ol>
<li><p>通过阅读 Gc 日志，我们可以了解 Java 虚拟机内存分配与回收策略。 内存分配与垃圾回收的参数列表</p>
<ul>
<li><code>-XX:+PrintGC</code> 输出 GC 日志。类似：<code>-verbose:gc</code></li>
<li><code>-XX:+PrintGCDetails</code> 输出 GC 的详细日志</li>
<li><code>-XX:+PrintGCTimestamps</code> 输出 GC 的时间戳（以基准时间的形式）</li>
<li><code>-XX:+PrintGCDatestamps</code> 输出 GcC 的时间戳（以日期的形式，如 2013-05-04T21：53：59.234+0800）</li>
<li><code>-XX:+PrintHeapAtGC</code> 在进行 GC 的前后打印出堆的信息</li>
<li><code>-Xloggc:../logs/gc.log</code> 日志文件的输出路径</li>
</ul>
</li>
<li><p>GC 日志举例</p>
<ul>
<li><p><code>-verbose:gc</code></p>
<blockquote>
<p>打印输出</p>
</blockquote>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line">[GC (Allocation Failure) 80832K-&gt;19298K(227840K),<span class="number">0.0084018</span> secs]</span><br><span class="line">[GC (Metadata GC Threshold) 109499K-&gt;21465K(228352K),<span class="number">0.0184066</span> secs]</span><br><span class="line">[Full <span class="title function_">GC</span> <span class="params">(Metadata GC Threshold)</span> 21465K-&gt;16716K(201728K),<span class="number">0.0619261</span> secs]</span><br></pre></td></tr></table></figure>

<blockquote>
<p>参数解析</p>
</blockquote>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line">GC、Full GC：GC的类型，GC只在新生代上进行，Full GC包括永生代，新生代，老年代</span><br><span class="line">Allocation Failure：GC发生的原因</span><br><span class="line">80832K-&gt;19298K：堆在GC前的大小和GC后的大小</span><br><span class="line">228840k：现在的堆大小</span><br><span class="line"><span class="number">0.0084018</span> secs：GC持续的时间</span><br></pre></td></tr></table></figure>
</li>
<li><p><code>-verbose:gc -XX:+PrintGCDetails</code></p>
<blockquote>
<p>打印输出</p>
</blockquote>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line">[GC (Allocation Failure) [PSYoungGen:70640K-&gt;10116K(141312K)] 80541K-&gt;20017K(227328K),<span class="number">0.0172573</span> secs] [Times:user=<span class="number">0.03</span> sys=<span class="number">0.00</span>,real=<span class="number">0.02</span> secs]</span><br><span class="line">[GC (Metadata GC Threshold) [PSYoungGen:98859K-&gt;8154K(142336K)] 108760K-&gt;21261K(228352K),<span class="number">0.0151573</span> secs] [Times:user=<span class="number">0.00</span> sys=<span class="number">0.01</span>,real=<span class="number">0.02</span> secs]</span><br><span class="line">[Full <span class="title function_">GC</span> <span class="params">(Metadata GC Threshold)</span>[PSYoungGen:8154K-&gt;0K(142336K)]</span><br><span class="line">[ParOldGen:13107K-&gt;16809K(62464K)] 21261K-&gt;16809K(204800K),[Metaspace:20599K-&gt;20599K(1067008K)],<span class="number">0.0639732</span> secs]</span><br><span class="line">[Times:user=<span class="number">0.14</span> sys=<span class="number">0.00</span>,real=<span class="number">0.06</span> secs]</span><br></pre></td></tr></table></figure>

<blockquote>
<p>参数解析</p>
</blockquote>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line">GC，Full FC：同样是GC的类型</span><br><span class="line">Allocation Failure：GC原因</span><br><span class="line">PSYoungGen：使用了Parallel Scavenge并行垃圾收集器的新生代GC前后大小的变化</span><br><span class="line">ParOldGen：使用了Parallel Old并行垃圾收集器的老年代GC前后大小的变化</span><br><span class="line">Metaspace： 元数据区GC前后大小的变化，JDK1<span class="number">.8</span>中引入了元数据区以替代永久代</span><br><span class="line">xxx secs：指GC花费的时间</span><br><span class="line">Times：user：指的是垃圾收集器花费的所有CPU时间，sys：花费在等待系统调用或系统事件的时间，real：GC从开始到结束的时间，包括其他进程占用时间片的实际时间。</span><br></pre></td></tr></table></figure>
</li>
<li><p><code>-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimestamps -XX:+PrintGCDatestamps</code></p>
<blockquote>
<p>打印输出（带上了日期和时间）</p>
</blockquote>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="number">2019</span>-09-24T22:<span class="number">15</span>:<span class="number">24.518</span>+0800: <span class="number">3.287</span>: [GC (Allocation Failure) [PSYoungGen:136162K-&gt;5113K(136192K)] 141425K-&gt;17632K(222208K),<span class="number">0.0248249</span> secs] [Times:user=<span class="number">0.05</span> sys=<span class="number">0.00</span>,real=<span class="number">0.03</span> secs]</span><br><span class="line"></span><br><span class="line"><span class="number">2019</span>-09-24T22:<span class="number">15</span>:<span class="number">25.559</span>+0800: <span class="number">4.329</span>: [GC (Metadata GC Threshold) [PSYoungGen:97578K-&gt;10068K(274944K)] 110096K-&gt;22658K(360960K),<span class="number">0.0094071</span> secs] [Times: user=<span class="number">0.00</span> sys=<span class="number">0.00</span>,real=<span class="number">0.01</span> secs]</span><br><span class="line"></span><br><span class="line"><span class="number">2019</span>-09-24T22:<span class="number">15</span>:<span class="number">25.569</span>+0800: <span class="number">4.338</span>: [Full <span class="title function_">GC</span> <span class="params">(Metadata GC Threshold)</span> [PSYoungGen:10068K-&gt;0K(274944K)][ParoldGen:12590K-&gt;13564K(56320K)] 22658K-&gt;13564K(331264K),[Metaspace:20590K-&gt;20590K(1067008K)],<span class="number">0.0494875</span> secs] [Times: user=<span class="number">0.17</span> sys=<span class="number">0.02</span>,real=<span class="number">0.05</span> secs]</span><br></pre></td></tr></table></figure></li>
</ul>
</li>
<li><p>日志补充说明</p>
<blockquote>
<p>如果想把 GC 日志存到文件的话，是下面的参数：</p>
<p><code>-Xloggc:/path/to/gc.log</code></p>
</blockquote>
<ul>
<li><p><code>[GC</code> 和 <code>[Full GC</code> 说明了这次垃圾收集的停顿类型，如果有 <code>Full GC</code> 则说明 GC 发生了 <code>Stop The World</code></p>
</li>
<li><p>使用 Serial 收集器在新生代的名字是 Default New Generation，因此显示的是 <code>[DefNew</code></p>
</li>
<li><p>使用 ParNew 收集器在新生代的名字会变成 <code>[ParNew</code>，意思是 <code>Parallel New Generation</code></p>
</li>
<li><p>使用 Parallel scavenge 收集器在新生代的名字是 <code>[PSYoungGen</code></p>
</li>
<li><p>老年代的收集和新生代道理一样，名字也是收集器决定的</p>
</li>
<li><p>使用 G1 收集器的话，会显示为 <code>garbage-first heap</code></p>
</li>
<li><p><strong>Allocation Failure</strong></p>
<blockquote>
<p>表明本次引起 GC 的原因是因为在年轻代中没有足够的空间能够存储新的数据了</p>
</blockquote>
</li>
<li><p><strong>[PSYoungGen：5986K-&gt;696K(8704K) ] 5986K-&gt;704K(9216K)</strong></p>
<blockquote>
<p>中括号内：GC 回收前年轻代大小，回收后大小，（年轻代总大小）</p>
<p>括号外：GC 回收前年轻代和老年代大小，回收后大小，（年轻代和老年代总大小）</p>
</blockquote>
</li>
<li><p>user 代表用户态回收耗时，sys 内核态回收耗时，real 实际耗时</p>
<blockquote>
<p>由于多核的原因，时间总和可能会超过 real 时间</p>
</blockquote>
</li>
</ul>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line">Heap（堆）</span><br><span class="line">PSYoungGen（Parallel Scavenge收集器新生代）total 9216K，used 6234K [<span class="number">0x00000000ff600000</span>,<span class="number">0x0000000100000000</span>,<span class="number">0x0000000100000000</span>)</span><br><span class="line">eden space（堆中的Eden区默认占比是<span class="number">8</span>）8192K，<span class="number">768</span> used [<span class="number">0x00000000ff600000</span>,<span class="number">0x00000000ffc16b08</span>,<span class="number">0x00000000ffe00000</span>)</span><br><span class="line">from space（堆中的Survivor，这里是From Survivor区默认占比是<span class="number">1</span>）1024K， <span class="number">0</span>% used [<span class="number">0x00000000fff00000</span>,<span class="number">0x00000000fff00000</span>,<span class="number">0x0000000100000000</span>)</span><br><span class="line">to space（堆中的Survivor，这里是to Survivor区默认占比是<span class="number">1</span>，需要先了解一下堆的分配策略）1024K, <span class="number">0</span>% used [<span class="number">0x00000000ffe00000</span>,<span class="number">0x00000000ffe00000</span>,<span class="number">0x00000000fff00000</span>)</span><br><span class="line"></span><br><span class="line">ParOldGen（老年代总大小和使用大小）total 10240K， used 7001K ［<span class="number">0x00000000fec00000</span>,<span class="number">0x00000000ff600000</span>,<span class="number">0x00000000ff600000</span>)</span><br><span class="line">object space（显示个使用百分比）10240K，<span class="number">688</span> used [<span class="number">0x00000000fec00000</span>,<span class="number">0x00000000ff2d6630</span>,<span class="number">0x00000000ff600000</span>)</span><br><span class="line"></span><br><span class="line">PSPermGen（永久代总大小和使用大小）total 21504K， used 4949K [<span class="number">0x00000000f9a00000</span>,<span class="number">0x00000000faf00000</span>,<span class="number">0x00000000fec00000</span>)</span><br><span class="line">object space（显示个使用百分比，自己能算出来）21504K， <span class="number">238</span> used [<span class="number">0x00000000f9a00000</span>,<span class="number">0x00000000f9ed55e0</span>,<span class="number">0x00000000faf00000</span>)</span><br></pre></td></tr></table></figure>
</li>
<li><p>Minor GC 和 Full GC</p>
<ul>
<li><p>Minor GC</p>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/MinorGC.png" alt="MinorGC"></p>
</li>
<li><p>Full GC</p>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/FullGC.png" alt="FullGC"></p>
</li>
<li><p>举例</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">int</span> <span class="variable">_1MB</span> <span class="operator">=</span> <span class="number">1024</span> * <span class="number">1024</span>;</span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">testAllocation</span><span class="params">()</span> &#123;    </span><br><span class="line">	<span class="type">byte</span> [] allocation1, allocation2, allocation3, allocation4;</span><br><span class="line">    allocation1 = <span class="keyword">new</span> <span class="title class_">byte</span>[<span class="number">2</span> *_1MB];</span><br><span class="line">    allocation2 = <span class="keyword">new</span> <span class="title class_">byte</span>[<span class="number">2</span> *_1MB];</span><br><span class="line">    allocation3 = <span class="keyword">new</span> <span class="title class_">byte</span>[<span class="number">2</span> *_1MB];</span><br><span class="line">    allocation4 = <span class="keyword">new</span> <span class="title class_">byte</span>[<span class="number">4</span> *_1MB];</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">    testAllocation();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<blockquote>
<p>设置参数</p>
<p><code>-Xms10m -Xmx10m -XX:+PrintGCDetails</code></p>
</blockquote>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/GC%E4%B8%BE%E4%BE%8B1.png" alt="GC举例1"></p>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/GC%E4%B8%BE%E4%BE%8B2.png" alt="GC举例2"></p>
<blockquote>
<p>可以用一些工具去分析这些 GC 日志</p>
<p>常用的日志分析工具有：<code>GCViewer、GCEasy、GCHisto、GCLogViewer、Hpjmeter、garbagecat</code> 等</p>
</blockquote>
</li>
</ul>
</li>
</ol>
<h4 id="10-垃圾回收器的新发展"><a href="#10-垃圾回收器的新发展" class="headerlink" title="10.垃圾回收器的新发展"></a>10.垃圾回收器的新发展</h4><blockquote>
<p>GC 仍然处于飞速发展之中，目前的默认选项G1 GC 在不断的进行改进，很多我们原来认为的缺点，例如串行的 Fu11GC、Card Table 扫描的低效等，都已经被大幅改进，例如，JDK10 以后，Fu11GC 已经是并行运行，在很多场景下，其表现还略优于 ParallelGC 的并行 Ful1GC 实现</p>
<p>即使是 Serial GC，虽然比较古老，但是简单的设计和实现未必就是过时的，它本身的开销，不管是 GC 相关数据结构的开销，还是线程的开销，都是非常小的，所以随着云计算的兴起，<strong>在 Serverless 等新的应用场景下，Serial GC 找到了新的舞台</strong></p>
<p>比较不幸的是 CMS GC，因为其算法的理论缺陷等原因，虽然现在还有非常大的用户群体，但在 JDK9 中已经被标记为废弃，并在 JDK14 版本中移除</p>
</blockquote>
<ol>
<li><p>JDK11 新特性</p>
<ul>
<li><a target="_blank" rel="noopener" href="http://openidk.iava.net/jeps/318">Epsilon:A No-Op GarbageCollector</a>（Epsilon 垃圾回收器，”No-Op（无操作）”回收器）</li>
<li><a target="_blank" rel="noopener" href="http://openidk.iava.net/jeps/318">ZGC:A Scalable Low-Latency Garbage Collector（Experimental）</a>（ZGC：可伸缩的低延迟垃圾回收器，处于实验性阶段）</li>
</ul>
</li>
<li><p>Open JDK12 的 Shenandoash GC</p>
<blockquote>
<p>现在 G1 回收器已成为默认回收器好几年了</p>
<p>两个新的收集器：ZGC（JDK11 出现）和 Shenandoah（Open JDK12）</p>
<p>主打特点：低停顿时间</p>
</blockquote>
<blockquote>
<p><strong>Open JDK12 的 Shenandoash GC：低停顿时间的 GC（实验性）</strong></p>
<p><strong>Shenandoah，无疑是众多 GC 中最孤独的一个</strong>。是第一款不由 oracle 公司团队领导开发的 Hotspot 垃圾收集器。<strong>不可避免的受到官方的排挤</strong></p>
<p>比如号称 OpenJDK 和 OracleJDK 没有区别的 Oracle 公司仍拒绝在 OracleJDK12 中支持 Shenandoah</p>
<p>Shenandoah 垃圾回收器最初由 RedHat 进行的一项垃圾收集器研究项目 Pauseless GC 的实现，<strong>旨在针对 JVM 上的内存回收实现低停顿的需求</strong>。在 2014 年贡献给 OpenJDK</p>
<p>Red Hat 研发 Shenandoah 团队对外宣称，<strong>Shenandoah 垃圾回收器的暂停时间与堆大小无关，这意味着无论将堆设置为 200MB 还是 200GB，99.9%的目标都可以把垃圾收集的停顿时间限制在十毫秒以内</strong>。不过实际使用性能将取决于实际工作堆的大小和工作负载</p>
</blockquote>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/Shenandoah.png" alt="Shenandoah"></p>
<ul>
<li><p>这是 RedHat 在 2016 年发表的论文数据，测试内容是使用 Es 对 200GB 的维基百科数据进行索引</p>
<ul>
<li>停顿时间比其他几款收集器确实有了质的飞跃，但也未实现最大停顿时间控制在十毫秒以内的目标</li>
<li>而吞吐量方面出现了明显的下降，总运行时间是所有测试收集器里最长的</li>
</ul>
</li>
<li><p>总结</p>
<ul>
<li>Shenandoah GC 的弱项：高运行负担下的吞吐量下降</li>
<li>Shenandoah GC 的强项：低延迟时间</li>
<li>Shenandoah GC 的工作过程大致分为九个阶段，这里就不再赘述。在之前 <a target="_blank" rel="noopener" href="https://www.bilibili.com/video/BV1jJ411M7kQ?from=search&seid=12339069673726242866">Java12 新特性视频</a>里有过介绍</li>
</ul>
</li>
</ul>
</li>
<li><p>令人震惊、革命性的 ZGC</p>
<ul>
<li><p><a target="_blank" rel="noopener" href="https://docs.oracle.com/en/java/javase/12/gctuning/">官方网址</a></p>
<blockquote>
<p>ZGC 与 Shenandoah 目标高度相似，<strong>在尽可能对吞吐量影响不大的前提下，实现在任意堆内存大小下都可以把垃圾收集的停颇时间限制在十毫秒以内的低延迟</strong></p>
<p>《深入理解 Java 虚拟机》一书中这样定义 ZGC：ZGC 收集器是一款基于 Region 内存布局的，（暂时）不设分代的，使用了读屏障、染色指针和内存多重映射等技术来<strong>实现可并发的标记-压缩算法的</strong>，<strong>以低延迟为首要目标</strong>的一款垃圾收集器</p>
<p>ZGC 的工作过程可以分为 4 个阶段：<strong>并发标记 - 并发预备重分配 - 并发重分配 - 并发重映射</strong>等</p>
<p>ZGC 几乎在所有地方并发执行的，除了初始标记的是 STW 的。所以停顿时间几乎就耗费在初始标记上，这部分的实际时间是非常少的</p>
<p>在 ZGC 的强项停顿时间测试上，它毫不留情的将 Parallel、G1 拉开了两个数量级的差距。无论平均停顿、95％停顿、99％停顿、99.9％停顿，还是最大停顿时间，ZGC 都能毫不费劲控制在 10 毫秒以内</p>
<p>虽然 ZGC 还在试验状态，没有完成所有特性，但此时性能已经相当亮眼，用令人震惊、革命性来形容不为过</p>
<p> <strong>未来将在服务端、大内存、低延迟应用的首选垃圾收集器</strong></p>
</blockquote>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/ZGC.png" alt="ZGC"></p>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/ZGC2.png" alt="ZGC2"></p>
</li>
</ul>
</li>
<li><p>其他垃圾回收器：AliGC</p>
<blockquote>
<p>AliGC 是阿里巴巴 JVM 团队基于 G1 算法，面向大堆（LargeHeap）应用场景。指定场景下的对比：</p>
</blockquote>
<p><img src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/AliGC.png" alt="AliGC"></p>
<blockquote>
<p>当然，其它厂商也提供了各种别具一格的 GC 实现，例如比较有名的低延迟 <a target="_blank" rel="noopener" href="https://www.infoq.com/articles/azul_gc_in_detail">GC：Zing</a>，有兴趣可以参考提供的链接</p>
</blockquote>
</li>
</ol>
</article><div class="post-copyright"><div class="post-copyright__author"><span class="post-copyright-meta">文章作者: </span><span class="post-copyright-info"><a href="https://gitee.com/zcmmmm/zcmmmm">zcm</a></span></div><div class="post-copyright__type"><span class="post-copyright-meta">文章链接: </span><span class="post-copyright-info"><a href="https://gitee.com/zcmmmm/zcmmmm/2022/10/07/JVM%20%E7%95%AA%E5%A4%96-%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E7%8B%AC%E7%AB%8B%E7%AF%87/">https://gitee.com/zcmmmm/zcmmmm/2022/10/07/JVM%20%E7%95%AA%E5%A4%96-%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E7%8B%AC%E7%AB%8B%E7%AF%87/</a></span></div><div class="post-copyright__notice"><span class="post-copyright-meta">版权声明: </span><span class="post-copyright-info">本博客所有文章除特别声明外，均采用 <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/" target="_blank">CC BY-NC-SA 4.0</a> 许可协议。转载请注明来自 <a href="https://gitee.com/zcmmmm/zcmmmm" target="_blank">无人深空</a>！</span></div></div><div class="tag_share"><div class="post-meta__tag-list"><a class="post-meta__tags" href="/tags/JVM/">JVM</a></div><div class="post_share"><div class="social-share" data-image="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/JVM.jpg" data-sites="facebook,twitter,wechat,weibo,qq"></div><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/butterfly-extsrc/sharejs/dist/css/share.min.css" media="print" onload="this.media='all'"><script src="https://cdn.jsdelivr.net/npm/butterfly-extsrc/sharejs/dist/js/social-share.min.js" defer></script></div></div><nav class="pagination-post" id="pagination"><div class="prev-post pull-left"><a href="/2022/10/08/JVM%20%E4%B8%AD%E7%AF%87-%E5%AD%97%E8%8A%82%E7%A0%81%E4%B8%8E%E7%B1%BB%E7%9A%84%E5%8A%A0%E8%BD%BD%E7%AF%87/"><img class="prev-cover" src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/JVM.jpg" onerror="onerror=null;src='/img/404.jpg'" alt="cover of previous post"><div class="pagination-info"><div class="label">上一篇</div><div class="prev_info">JVM 中篇-字节码与类的加载篇</div></div></a></div><div class="next-post pull-right"><a href="/2022/09/28/JVM%20%E4%B8%8A%E7%AF%87-%E5%86%85%E5%AD%98%E4%B8%8E%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6/"><img class="next-cover" src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/JVM.jpg" onerror="onerror=null;src='/img/404.jpg'" alt="cover of next post"><div class="pagination-info"><div class="label">下一篇</div><div class="next_info">JVM 上篇-内存与垃圾回收</div></div></a></div></nav><div class="relatedPosts"><div class="headline"><i class="fas fa-thumbs-up fa-fw"></i><span>相关推荐</span></div><div class="relatedPosts-list"><div><a href="/2022/10/11/JVM%20%E4%B8%8B%E7%AF%87-%E6%80%A7%E8%83%BD%E7%9B%91%E6%8E%A7%E4%B8%8E%E8%B0%83%E4%BC%98%E7%AF%87/" title="JVM 下篇-性能监控与调优篇"><img class="cover" src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/JVM.jpg" alt="cover"><div class="content is-center"><div class="date"><i class="far fa-calendar-alt fa-fw"></i> 2022-10-11</div><div class="title">JVM 下篇-性能监控与调优篇</div></div></a></div><div><a href="/2022/09/28/JVM%20%E4%B8%8A%E7%AF%87-%E5%86%85%E5%AD%98%E4%B8%8E%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6/" title="JVM 上篇-内存与垃圾回收"><img class="cover" src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/JVM.jpg" alt="cover"><div class="content is-center"><div class="date"><i class="far fa-calendar-alt fa-fw"></i> 2022-09-28</div><div class="title">JVM 上篇-内存与垃圾回收</div></div></a></div><div><a href="/2022/10/08/JVM%20%E4%B8%AD%E7%AF%87-%E5%AD%97%E8%8A%82%E7%A0%81%E4%B8%8E%E7%B1%BB%E7%9A%84%E5%8A%A0%E8%BD%BD%E7%AF%87/" title="JVM 中篇-字节码与类的加载篇"><img class="cover" src="https://study-record-images.oss-cn-beijing.aliyuncs.com/Java/JVM/JVM.jpg" alt="cover"><div class="content is-center"><div class="date"><i class="far fa-calendar-alt fa-fw"></i> 2022-10-08</div><div class="title">JVM 中篇-字节码与类的加载篇</div></div></a></div></div></div></div><div class="aside-content" id="aside-content"><div class="sticky_layout"><div class="card-widget" id="card-toc"><div class="item-headline"><i class="fas fa-stream"></i><span>目录</span><span class="toc-percentage"></span></div><div class="toc-content is-expand"><ol class="toc"><li class="toc-item toc-level-2"><a class="toc-link" href="#JVM-%E4%B8%8A%E7%AF%87-%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6"><span class="toc-text">JVM 上篇-垃圾回收</span></a><ol class="toc-child"><li class="toc-item toc-level-3"><a class="toc-link" href="#1-%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%A6%82%E8%BF%B0"><span class="toc-text">1.垃圾回收概述</span></a><ol class="toc-child"><li class="toc-item toc-level-4"><a class="toc-link" href="#1-%E4%BB%80%E4%B9%88%E6%98%AF%E5%9E%83%E5%9C%BE"><span class="toc-text">1.什么是垃圾</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#2-%E4%B8%BA%E4%BB%80%E4%B9%88%E9%9C%80%E8%A6%81-GC"><span class="toc-text">2.为什么需要 GC</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#3-%E6%97%A9%E6%9C%9F%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6"><span class="toc-text">3.早期垃圾回收</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#4-Java-%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6"><span class="toc-text">4.Java 垃圾回收机制</span></a></li></ol></li><li class="toc-item toc-level-3"><a class="toc-link" href="#2-%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E7%9B%B8%E5%85%B3%E7%AE%97%E6%B3%95"><span class="toc-text">2.垃圾回收相关算法</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#3-%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E7%9B%B8%E5%85%B3%E6%A6%82%E5%BF%B5"><span class="toc-text">3.垃圾回收相关概念</span></a><ol class="toc-child"><li class="toc-item toc-level-4"><a class="toc-link" href="#1-System-gc-%E7%9A%84%E7%90%86%E8%A7%A3"><span class="toc-text">1.System.gc() 的理解</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#2-%E5%86%85%E5%AD%98%E6%BA%A2%E5%87%BA%E4%B8%8E%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F"><span class="toc-text">2.内存溢出与内存泄漏</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#3-Stop-The-World"><span class="toc-text">3.Stop The World</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#4-%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E7%9A%84%E5%B9%B6%E8%A1%8C%EF%BC%88Parallel%EF%BC%89%E4%B8%8E%E5%B9%B6%E5%8F%91%EF%BC%88Concurrent%EF%BC%89"><span class="toc-text">4.垃圾回收的并行（Parallel）与并发（Concurrent）</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#5-%E5%AE%89%E5%85%A8%E7%82%B9%E4%B8%8E%E5%AE%89%E5%85%A8%E5%8C%BA%E5%9F%9F"><span class="toc-text">5.安全点与安全区域</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#6-%E5%9B%9B%E5%A4%A7%E5%BC%95%E7%94%A8%EF%BC%88%E9%9D%A2%E8%AF%95%E9%87%8D%E7%82%B9%EF%BC%89"><span class="toc-text">6.四大引用（面试重点）</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#7-%E5%BC%BA%E5%BC%95%E7%94%A8%EF%BC%88Strong-Reference%EF%BC%89%E2%80%94%E2%80%94%E4%B8%8D%E5%9B%9E%E6%94%B6"><span class="toc-text">7.强引用（Strong Reference）——不回收</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#8-%E8%BD%AF%E5%BC%95%E7%94%A8%EF%BC%88Soft-Reference%EF%BC%89%E2%80%94%E2%80%94%E5%86%85%E5%AD%98%E4%B8%8D%E8%B6%B3%E5%8D%B3%E5%9B%9E%E6%94%B6"><span class="toc-text">8.软引用（Soft Reference）——内存不足即回收</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#9-%E5%BC%B1%E5%BC%95%E7%94%A8%EF%BC%88Weak-Reference%EF%BC%89%E2%80%94%E2%80%94%E5%8F%91%E7%8E%B0%E5%8D%B3%E5%9B%9E%E6%94%B6"><span class="toc-text">9.弱引用（Weak Reference）——发现即回收</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#10-%E8%99%9A%E5%BC%95%E7%94%A8%EF%BC%88Phantom-Reference%EF%BC%89%E2%80%94%E2%80%94%E5%AF%B9%E8%B1%A1%E5%9B%9E%E6%94%B6%E8%B7%9F%E8%B8%AA"><span class="toc-text">10.虚引用（Phantom Reference）——对象回收跟踪</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#11-%E7%BB%88%E7%BB%93%E5%99%A8%E5%BC%95%E7%94%A8"><span class="toc-text">11.终结器引用</span></a></li></ol></li><li class="toc-item toc-level-3"><a class="toc-link" href="#4-%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E5%99%A8"><span class="toc-text">4.垃圾回收器</span></a><ol class="toc-child"><li class="toc-item toc-level-4"><a class="toc-link" href="#1-GC-%E5%88%86%E7%B1%BB%E4%B8%8E%E6%80%A7%E8%83%BD%E6%8C%87%E6%A0%87"><span class="toc-text">1.GC 分类与性能指标</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#2-%E4%B8%8D%E5%90%8C%E7%9A%84%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E5%99%A8%E6%A6%82%E8%BF%B0"><span class="toc-text">2.不同的垃圾回收器概述</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#3-Serial-x2F-Serial-Old-%E5%9B%9E%E6%94%B6%E5%99%A8%EF%BC%9A%E4%B8%B2%E8%A1%8C%E5%9B%9E%E6%94%B6"><span class="toc-text">3.Serial &#x2F; Serial Old 回收器：串行回收</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#4-ParNew-%E5%9B%9E%E6%94%B6%E5%99%A8%EF%BC%9A%E5%B9%B6%E8%A1%8C%E5%9B%9E%E6%94%B6"><span class="toc-text">4.ParNew 回收器：并行回收</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#5-Parallel-Scavenge-x2F-Parallel-Old-%E5%9B%9E%E6%94%B6%E5%99%A8%EF%BC%9A%E5%90%9E%E5%90%90%E9%87%8F%E4%BC%98%E5%85%88"><span class="toc-text">5.Parallel Scavenge &#x2F; Parallel Old 回收器：吞吐量优先</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#6-CMS-%E5%9B%9E%E6%94%B6%E5%99%A8%EF%BC%9A%E4%BD%8E%E5%BB%B6%E8%BF%9F"><span class="toc-text">6.CMS 回收器：低延迟</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#%E5%B0%8F%E7%BB%93"><span class="toc-text">小结</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#7-G1-%E5%9B%9E%E6%94%B6%E5%99%A8%EF%BC%9A%E5%8C%BA%E5%9F%9F%E5%8C%96%E5%88%86%E4%BB%A3%E5%BC%8F"><span class="toc-text">7.G1 回收器：区域化分代式</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#8-%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E5%99%A8%E6%80%BB%E7%BB%93"><span class="toc-text">8.垃圾回收器总结</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#9-GC-%E6%97%A5%E5%BF%97%E5%88%86%E6%9E%90"><span class="toc-text">9.GC 日志分析</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#10-%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E5%99%A8%E7%9A%84%E6%96%B0%E5%8F%91%E5%B1%95"><span class="toc-text">10.垃圾回收器的新发展</span></a></li></ol></li></ol></li></ol></div></div></div></div></main><footer id="footer"><div id="footer-wrap"><div class="copyright">&copy;2022 - 2023 By zcm</div><div class="framework-info"><span>框架 </span><a target="_blank" rel="noopener" href="https://hexo.io">Hexo</a><span class="footer-separator">|</span><span>主题 </span><a target="_blank" rel="noopener" href="https://github.com/jerryc127/hexo-theme-butterfly">Butterfly</a></div></div></footer></div><div id="rightside"><div id="rightside-config-hide"><button id="readmode" type="button" title="阅读模式"><i class="fas fa-book-open"></i></button><button id="darkmode" type="button" title="浅色和深色模式转换"><i class="fas fa-adjust"></i></button><button id="hide-aside-btn" type="button" title="单栏和双栏切换"><i class="fas fa-arrows-alt-h"></i></button></div><div id="rightside-config-show"><button id="rightside_config" type="button" title="设置"><i class="fas fa-cog fa-spin"></i></button><button class="close" id="mobile-toc-button" type="button" title="目录"><i class="fas fa-list-ul"></i></button><button id="go-up" type="button" title="回到顶部"><i class="fas fa-arrow-up"></i></button></div></div><div id="local-search"><div class="search-dialog"><nav class="search-nav"><span class="search-dialog-title">搜索</span><span id="loading-status"></span><button class="search-close-button"><i class="fas fa-times"></i></button></nav><div class="is-center" id="loading-database"><i class="fas fa-spinner fa-pulse"></i><span>  数据库加载中</span></div><div class="search-wrap"><div id="local-search-input"><div class="local-search-box"><input class="local-search-box--input" placeholder="搜索文章" type="text"/></div></div><hr/><div id="local-search-results"></div></div></div><div id="search-mask"></div></div><div><script src="/js/utils.js"></script><script src="/js/main.js"></script><script src="https://cdn.jsdelivr.net/npm/@fancyapps/ui/dist/fancybox.umd.min.js"></script><script src="https://cdn.jsdelivr.net/npm/node-snackbar/dist/snackbar.min.js"></script><script src="/js/search/local-search.js"></script><div class="js-pjax"></div><script src="/js/categories.js?v1"></script><script src="/js/navigation.js?v1"></script><script async data-pjax src="//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js"></script></div></body></html>